Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions docs/writeback-evolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ credentials:
"id": "git",
"type": "git",
"repository": "project",
"allBranches": true,
"defaultBranch": "main",
"freshnessThresholdHours": 168,
"paths": [".ai", "AGENTS.md", "docs"]
},
{
Expand Down Expand Up @@ -120,7 +121,7 @@ the requested review window:
"observedAt": "2026-07-08T14:30:00Z",
"body": "Implementation completed",
"refs": ["EXAMPLE-123"],
"terminal": true
"status": "done"
}
]
}
Expand All @@ -133,6 +134,24 @@ user-owned exporter can produce that file from any external system.
Missing exports, missing logs, stale sources, and adapter failures produce
degraded coverage instead of a false empty result.

Coverage and cursor freshness are separate contracts. `coverageComplete`
means every configured source proved the requested window; it does not mean a
stored source cursor is current. JSON, persisted reports, and readable review
artifacts expose a typed `freshness` state independently for each source and
for the review overall. A cursor becomes stale only when its configured
`freshnessThresholdHours` is exceeded (168 hours by default) or a bounded Git
check finds newer activity on the configured default branch. Newer repository
activity is checked independently of `paths`, so a complete path-filtered scan
cannot mask a frozen Git cursor. An unchanged stale queue item is suppressed
after its first notification.

Terminal evidence-export statuses (`done`, `completed`, `canceled`,
`obsolete`, `duplicate`, and their supported aliases) resolve a linked signal
family when the bounded export proves all linked work terminal. Git evidence
resolves a family only when the exact commit or equivalent patch is contained
on the configured default branch. Both paths preserve source provenance and
remain read-only; they do not close tracker work, edit Git, or apply proposals.

Configure file sources narrowly around append-only logs, dated runbooks, or
research streams that represent review evidence. Date section headings as
`## YYYY-MM-DD ...` so one file can prove which observations belong to the
Expand Down
6 changes: 6 additions & 0 deletions src/activity-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ function report(args: {
generationAfter: 3,
coverage: [],
coverageComplete: true,
freshness: {
state: "current",
staleSourceIds: [],
unknownSourceIds: [],
alertSourceIds: [],
},
queue: [item],
delta: {
new: [item.id],
Expand Down
14 changes: 14 additions & 0 deletions src/activity-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,18 @@ function review(args: {
},
coverageComplete: true,
degraded: false,
freshness: {
state: "current",
staleSourceIds: [],
unknownSourceIds: [],
alertSourceIds: [],
},
coverage: [],
decisions: [],
evidence: [],
signals: [],
resolutionProofs: [],
resolvedSignalFamilies: [],
resolvedEvidenceKeys: [],
unresolvedSignals: [],
linkedWork: [],
Expand Down Expand Up @@ -213,6 +221,12 @@ function report(args: {
reviewId: `review-${args.index}`,
coverage: [],
coverageComplete: status === "complete",
freshness: {
state: status === "complete" ? "current" : "unknown",
staleSourceIds: [],
unknownSourceIds: [],
alertSourceIds: [],
},
queue: [],
delta: {
new: [],
Expand Down
16 changes: 16 additions & 0 deletions src/activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,22 @@ function report(overrides?: Partial<EvolutionLoopReport>): EvolutionLoopReport {
checkedAt: "2026-07-13T00:00:00.000Z",
recordsScanned: 2,
signalsDiscovered: 1,
freshness: {
state: "current",
reason: "cursor_advanced",
checkedAt: "2026-07-13T00:00:00.000Z",
thresholdHours: 168,
alert: false,
},
},
],
coverageComplete: true,
freshness: {
state: "current",
staleSourceIds: [],
unknownSourceIds: [],
alertSourceIds: [],
},
queue: [queueItem()],
delta: {
new: ["family:SF-stable"],
Expand Down Expand Up @@ -95,6 +108,7 @@ function review(): ReconciliationReview {
},
coverageComplete: true,
degraded: false,
freshness: report().freshness,
coverage: report().coverage,
decisions: [],
evidence: [],
Expand All @@ -116,6 +130,8 @@ function review(): ReconciliationReview {
unresolved: true,
},
],
resolutionProofs: [],
resolvedSignalFamilies: [],
resolvedEvidenceKeys: [],
unresolvedSignals: ["SIG-1"],
linkedWork: ["TASK-1"],
Expand Down
42 changes: 41 additions & 1 deletion src/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,12 @@ export interface ActivityFeed {
complete: boolean;
checked: number;
degraded: number;
freshness?: EvolutionLoopReport["freshness"];
sources: Array<{
id: string;
label: string;
state: SourceCoverage["state"];
freshness?: SourceCoverage["freshness"];
detail?: string;
}>;
};
Expand Down Expand Up @@ -883,10 +885,12 @@ export function buildActivityFeed(args: {
complete: args.report.coverageComplete,
checked: args.report.coverage.length - degradedSources.length,
degraded: degradedSources.length,
freshness: args.report.freshness,
sources: args.report.coverage.map((entry) => ({
id: redactPortableActivityText(entry.sourceId),
label: redactPortableActivityText(sourceLabel(entry.sourceId)),
state: entry.state,
freshness: entry.freshness,
detail: entry.unavailableReason
? redactPortableActivityText(entry.unavailableReason)
: entry.staleReason
Expand Down Expand Up @@ -963,6 +967,9 @@ export function renderActivityFeed(feed: ActivityFeed): string {
`Activity — ${label}`,
`Last review: ${feed.generatedAt} · ${feed.run.status}`,
`Coverage: ${feed.coverage.checked}/${feed.coverage.sources.length} sources checked${feed.coverage.complete ? "" : " · incomplete"}`,
...(feed.coverage.freshness
? [`Freshness: ${feed.coverage.freshness.state}`]
: []),
`Changes: ${feed.counts.new} new · ${feed.counts.changed} changed · ${feed.counts.resolved} resolved · ${feed.counts.unchangedSuppressed} unchanged suppressed`,
"",
...(degraded.length
Expand Down Expand Up @@ -1056,6 +1063,36 @@ function isNonNegativeInteger(value: unknown): value is number {
return Number.isInteger(value) && Number(value) >= 0;
}

function isReconciliationFreshness(
value: unknown
): value is EvolutionLoopReport["freshness"] {
return (
isRecord(value) &&
(value.state === "current" ||
value.state === "stale" ||
value.state === "unknown") &&
isStringArray(value.staleSourceIds) &&
isStringArray(value.unknownSourceIds) &&
isStringArray(value.alertSourceIds)
);
}

function isSourceFreshness(
value: unknown
): value is SourceCoverage["freshness"] {
return (
isRecord(value) &&
(value.state === "current" ||
value.state === "stale" ||
value.state === "unknown" ||
value.state === "not_applicable") &&
typeof value.reason === "string" &&
typeof value.checkedAt === "string" &&
typeof value.thresholdHours === "number" &&
typeof value.alert === "boolean"
);
}

function isActivityItem(value: unknown): value is ActivityItem {
if (!isRecord(value)) {
return false;
Expand Down Expand Up @@ -1140,13 +1177,16 @@ export function isActivityFeed(value: unknown): value is ActivityFeed {
typeof value.coverage.complete === "boolean" &&
isNonNegativeInteger(value.coverage.checked) &&
isNonNegativeInteger(value.coverage.degraded) &&
(value.coverage.freshness === undefined ||
isReconciliationFreshness(value.coverage.freshness)) &&
Array.isArray(value.coverage.sources) &&
value.coverage.sources.every(
(entry) =>
isRecord(entry) &&
typeof entry.id === "string" &&
typeof entry.label === "string" &&
typeof entry.state === "string"
typeof entry.state === "string" &&
(entry.freshness === undefined || isSourceFreshness(entry.freshness))
) &&
isRecord(value.counts) &&
isNonNegativeInteger(value.counts.total) &&
Expand Down
3 changes: 3 additions & 0 deletions src/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,8 @@ async function loopCommand(argv: string[]) {
: [
`loop report: ${result.runId}`,
`status: ${result.status}`,
`coverage: ${result.coverageComplete ? "complete" : "degraded"}`,
`freshness: ${result.freshness.state}`,
`queue: ${result.queue.length}`,
`notifiable: ${result.delta.notifiable.length}`,
`artifact: ${result.artifactPath}`,
Expand Down Expand Up @@ -3720,6 +3722,7 @@ async function reviewCommand(argv: string[]): Promise<void> {
: [
`review: ${result.reviewId}`,
`coverage: ${result.coverageComplete ? "complete" : "degraded"}`,
`freshness: ${result.freshness.state}`,
`signals: ${result.signals.length}`,
`artifact: ${result.artifactPath}`,
].join("\n")
Expand Down
Loading
Loading