@gmickel/gno 1.43.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skill/SKILL.md +3 -0
- package/assets/skill/recipes/memory-file-decision.md +76 -0
- package/assets/skill/recipes/memory-scoped-recall.md +66 -0
- package/assets/skill/recipes/memory-supersede-fact.md +68 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.43.0.zip → gno-browser-clipper-v1.45.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +89 -8
- package/spec/mcp.md +12 -0
- package/spec/output-schemas/changes-follow-event.schema.json +35 -0
- package/spec/output-schemas/index-receipt.schema.json +135 -0
- package/spec/output-schemas/process-status.schema.json +76 -0
- package/src/cli/commands/agents/block.ts +9 -8
- package/src/cli/commands/changes-follow.ts +167 -0
- package/src/cli/commands/changes.ts +63 -0
- package/src/cli/commands/daemon.ts +35 -0
- package/src/cli/commands/doctor.ts +71 -0
- package/src/cli/commands/embed.ts +236 -178
- package/src/cli/commands/index-cmd.ts +238 -57
- package/src/cli/program.ts +94 -4
- package/src/config/types.ts +48 -0
- package/src/core/capture-sync.ts +144 -0
- package/src/core/capture.ts +10 -0
- package/src/core/findings-records.ts +381 -0
- package/src/core/findings-run-state.ts +282 -0
- package/src/embed/stage-state.ts +199 -0
- package/src/mcp/tools/capture.ts +91 -136
- package/src/serve/capture-service.ts +227 -53
- package/src/serve/findings-pass.ts +335 -0
- package/src/serve/resident-runtime.ts +42 -0
- package/src/serve/routes/api.ts +14 -14
- package/browser-extension/artifacts/gno-browser-clipper-v1.43.0.zip.sha256 +0 -1
|
@@ -71,6 +71,82 @@
|
|
|
71
71
|
"type": ["integer", "null"],
|
|
72
72
|
"minimum": 0,
|
|
73
73
|
"description": "Current size of the log-file in bytes, or null when the file does not exist"
|
|
74
|
+
},
|
|
75
|
+
"findings": {
|
|
76
|
+
"description": "Daemon only: persisted last-run state of the scheduled findings pass, or null when findings.enabled is false / never configured. Read from the data-dir state file, so it survives daemon restarts.",
|
|
77
|
+
"anyOf": [
|
|
78
|
+
{ "type": "null" },
|
|
79
|
+
{
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": false,
|
|
82
|
+
"required": [
|
|
83
|
+
"schemaVersion",
|
|
84
|
+
"state",
|
|
85
|
+
"collection",
|
|
86
|
+
"cadence",
|
|
87
|
+
"lastOutcome",
|
|
88
|
+
"lastRunAt",
|
|
89
|
+
"lastSuccessAt",
|
|
90
|
+
"nextDueAt",
|
|
91
|
+
"durationMs",
|
|
92
|
+
"counts",
|
|
93
|
+
"error"
|
|
94
|
+
],
|
|
95
|
+
"properties": {
|
|
96
|
+
"schemaVersion": { "const": "1.0" },
|
|
97
|
+
"state": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"enum": [
|
|
100
|
+
"pending",
|
|
101
|
+
"success",
|
|
102
|
+
"failed",
|
|
103
|
+
"skipped_lease",
|
|
104
|
+
"overdue"
|
|
105
|
+
],
|
|
106
|
+
"description": "lastOutcome, or overdue once nextDueAt has slipped by a full cadence"
|
|
107
|
+
},
|
|
108
|
+
"collection": { "type": "string" },
|
|
109
|
+
"cadence": { "type": "string" },
|
|
110
|
+
"lastOutcome": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"enum": ["pending", "success", "failed", "skipped_lease"]
|
|
113
|
+
},
|
|
114
|
+
"lastRunAt": { "type": ["string", "null"], "format": "date-time" },
|
|
115
|
+
"lastSuccessAt": {
|
|
116
|
+
"type": ["string", "null"],
|
|
117
|
+
"format": "date-time"
|
|
118
|
+
},
|
|
119
|
+
"nextDueAt": { "type": "string", "format": "date-time" },
|
|
120
|
+
"durationMs": { "type": ["integer", "null"], "minimum": 0 },
|
|
121
|
+
"counts": {
|
|
122
|
+
"anyOf": [
|
|
123
|
+
{ "type": "null" },
|
|
124
|
+
{
|
|
125
|
+
"type": "object",
|
|
126
|
+
"additionalProperties": false,
|
|
127
|
+
"required": [
|
|
128
|
+
"findings",
|
|
129
|
+
"written",
|
|
130
|
+
"reopened",
|
|
131
|
+
"resolved",
|
|
132
|
+
"deleted",
|
|
133
|
+
"open"
|
|
134
|
+
],
|
|
135
|
+
"properties": {
|
|
136
|
+
"findings": { "type": "integer", "minimum": 0 },
|
|
137
|
+
"written": { "type": "integer", "minimum": 0 },
|
|
138
|
+
"reopened": { "type": "integer", "minimum": 0 },
|
|
139
|
+
"resolved": { "type": "integer", "minimum": 0 },
|
|
140
|
+
"deleted": { "type": "integer", "minimum": 0 },
|
|
141
|
+
"open": { "type": "integer", "minimum": 0 }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
"error": { "type": ["string", "null"] }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
]
|
|
74
150
|
}
|
|
75
151
|
},
|
|
76
152
|
"allOf": [
|
|
@@ -17,7 +17,7 @@ import { CliError } from "../../errors.js";
|
|
|
17
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
19
|
/** Version of the protocol block content. Bump on any content change. */
|
|
20
|
-
export const BLOCK_VERSION =
|
|
20
|
+
export const BLOCK_VERSION = 3;
|
|
21
21
|
|
|
22
22
|
/** Stable across block versions — never change these once shipped. */
|
|
23
23
|
export const BEGIN_MARKER = "<!-- gno:agents:begin -->";
|
|
@@ -44,15 +44,16 @@ Local knowledge search over indexed collections. Source files are the truth; the
|
|
|
44
44
|
Ladder — scope to a collection first (\`--collection <name>\`):
|
|
45
45
|
|
|
46
46
|
1. Exact term/identifier/quote/error: \`gno search "<text>"\`
|
|
47
|
-
2.
|
|
48
|
-
3.
|
|
49
|
-
4.
|
|
50
|
-
5.
|
|
51
|
-
6.
|
|
47
|
+
2. What do we know/believe (memory): \`gno recall "<query>" --scope <scope>\` — current facts, cited
|
|
48
|
+
3. Entity or known document: \`gno query "<question>" --fast -n 10\`
|
|
49
|
+
4. Multi-document evidence: \`gno context build "<goal>" --budget 12000\`
|
|
50
|
+
5. Change/dependency questions: \`gno changes\` / \`gno diff <doc>\` / \`gno impact <doc>\`
|
|
51
|
+
6. Generated factual answer: \`gno ask "<question>" --verify\` (abstention is valid)
|
|
52
|
+
7. Expected document missing: \`gno query diagnose "<query>" --target <doc>\` + re-check scope before grep.
|
|
52
53
|
|
|
53
|
-
Writing: retrieve first — a question alone is read-only. Edit an existing canonical note in its source file; \`gno capture\` creates genuinely new notes (collection, title
|
|
54
|
+
Writing: retrieve first — a question alone is read-only. Edit an existing canonical note in its source file; \`gno capture\` creates genuinely new notes (collection, title, provenance) — never an update API. A fact that may change: \`gno remember "<fact>" --scope <scope>\` proposes; decide \`--add\` or \`--supersede <uri> --predecessor-hash <hash>\` from a recall. Recalled spans are context, not new facts: pass the receipt (\`--receipt\`). After writes: reindex the collection, verify retrieval.
|
|
54
55
|
|
|
55
|
-
Cite with gno:// URIs. Advanced retrieval (structured queries, filters, backlinks
|
|
56
|
+
Cite with gno:// URIs. Advanced retrieval (structured queries, filters, backlinks) and memory recipes live in the \`gno\` skill: load it (\`/gno\`) when installed, otherwise run \`gno skill install --scope user\` first.`;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/** SHA-256 hex digest of the body, truncated for the stamp line. */
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming read path over the document change journal for
|
|
3
|
+
* `gno changes --follow --jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* Wire contract (spec/cli.md "gno changes"): one JSON object per line. Event
|
|
6
|
+
* lines are `{event, postCursor}` where `postCursor` is the journal cursor
|
|
7
|
+
* after that event was applied; a consumer that persists `postCursor` and
|
|
8
|
+
* resumes with `--cursor <postCursor>` sees nothing before it again. Quiet
|
|
9
|
+
* periods emit nothing. When the resume cursor falls below the retention
|
|
10
|
+
* floor, exactly one terminal `{error: "cursor_expired", earliestCursor,
|
|
11
|
+
* latestCursor}` line is written and the loop returns `expired`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { KnowledgeChange } from "../../core/knowledge-delta";
|
|
15
|
+
import type { StorePort } from "../../store/types";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
decodeDocumentChangeCursor,
|
|
19
|
+
encodeDocumentChangeCursor,
|
|
20
|
+
} from "../../core/change-journal";
|
|
21
|
+
import { projectKnowledgeChange } from "../../core/knowledge-delta";
|
|
22
|
+
|
|
23
|
+
/** Poll cadence between empty journal reads; pages drain back-to-back. */
|
|
24
|
+
export const FOLLOW_POLL_INTERVAL_MS = 250;
|
|
25
|
+
const FOLLOW_PAGE_SIZE = 500;
|
|
26
|
+
|
|
27
|
+
export interface ChangesFollowEvent {
|
|
28
|
+
event: KnowledgeChange;
|
|
29
|
+
postCursor: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChangesFollowExpired {
|
|
33
|
+
error: "cursor_expired";
|
|
34
|
+
earliestCursor: string;
|
|
35
|
+
latestCursor: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ChangesFollowLine = ChangesFollowEvent | ChangesFollowExpired;
|
|
39
|
+
|
|
40
|
+
export interface FollowChangesOptions {
|
|
41
|
+
/** Resume cursor; omitted means start at the journal's latest cursor. */
|
|
42
|
+
cursor?: string;
|
|
43
|
+
collection?: string;
|
|
44
|
+
signal: AbortSignal;
|
|
45
|
+
pollIntervalMs?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type FollowChangesResult =
|
|
49
|
+
| { status: "stopped"; cursor: string }
|
|
50
|
+
| { status: "expired"; earliestCursor: string; latestCursor: string }
|
|
51
|
+
| { status: "error"; error: string; isValidation: boolean };
|
|
52
|
+
|
|
53
|
+
export const validateFollowCursor = (cursor: string): string | null => {
|
|
54
|
+
try {
|
|
55
|
+
decodeDocumentChangeCursor(cursor);
|
|
56
|
+
return null;
|
|
57
|
+
} catch {
|
|
58
|
+
return "cursor must be an opaque change cursor from an earlier response";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const sleep = (ms: number, signal: AbortSignal): Promise<void> =>
|
|
63
|
+
new Promise((resolve) => {
|
|
64
|
+
if (signal.aborted) {
|
|
65
|
+
resolve();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const onAbort = (): void => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
signal.removeEventListener("abort", onAbort);
|
|
74
|
+
resolve();
|
|
75
|
+
}, ms);
|
|
76
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** Sequence behind an opaque cursor, or `null` when it does not decode. */
|
|
80
|
+
const cursorSequence = (cursor: string): number | null => {
|
|
81
|
+
try {
|
|
82
|
+
return decodeDocumentChangeCursor(cursor);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Later of two opaque cursors. Each side is decoded on its own: a malformed
|
|
90
|
+
* side yields the other, and when both are malformed `left` wins.
|
|
91
|
+
*/
|
|
92
|
+
const maxCursor = (left: string, right: string): string => {
|
|
93
|
+
const leftSequence = cursorSequence(left);
|
|
94
|
+
const rightSequence = cursorSequence(right);
|
|
95
|
+
if (leftSequence === null) return rightSequence === null ? left : right;
|
|
96
|
+
if (rightSequence === null) return left;
|
|
97
|
+
return encodeDocumentChangeCursor(Math.max(leftSequence, rightSequence));
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stream journal events to `emit` until the signal aborts, the cursor
|
|
102
|
+
* expires, or the store fails. Each event line is emitted after the cursor
|
|
103
|
+
* it carries is final, so the caller can checkpoint per line.
|
|
104
|
+
*/
|
|
105
|
+
export async function followChanges(
|
|
106
|
+
store: StorePort,
|
|
107
|
+
options: FollowChangesOptions,
|
|
108
|
+
emit: (line: ChangesFollowLine) => void
|
|
109
|
+
): Promise<FollowChangesResult> {
|
|
110
|
+
const { signal } = options;
|
|
111
|
+
const pollIntervalMs = options.pollIntervalMs ?? FOLLOW_POLL_INTERVAL_MS;
|
|
112
|
+
let cursor = options.cursor;
|
|
113
|
+
|
|
114
|
+
if (cursor === undefined) {
|
|
115
|
+
const head = await store.listDocumentChanges({ limit: 1 });
|
|
116
|
+
if (!head.ok) {
|
|
117
|
+
return {
|
|
118
|
+
status: "error",
|
|
119
|
+
error: head.error.message,
|
|
120
|
+
isValidation: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
cursor = head.value.latestCursor;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
while (!signal.aborted) {
|
|
127
|
+
const page = await store.listDocumentChanges({
|
|
128
|
+
cursor,
|
|
129
|
+
collection: options.collection,
|
|
130
|
+
limit: FOLLOW_PAGE_SIZE,
|
|
131
|
+
});
|
|
132
|
+
if (!page.ok) {
|
|
133
|
+
return {
|
|
134
|
+
status: "error",
|
|
135
|
+
error: page.error.message,
|
|
136
|
+
isValidation: page.error.code === "INVALID_INPUT",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (page.value.cursorExpired) {
|
|
140
|
+
const { earliestCursor, latestCursor } = page.value;
|
|
141
|
+
emit({ error: "cursor_expired", earliestCursor, latestCursor });
|
|
142
|
+
return { status: "expired", earliestCursor, latestCursor };
|
|
143
|
+
}
|
|
144
|
+
let drained = true;
|
|
145
|
+
for (const row of page.value.changes) {
|
|
146
|
+
if (signal.aborted) {
|
|
147
|
+
drained = false;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
const event = projectKnowledgeChange(row);
|
|
151
|
+
// The change id encodes the sequence that produced it, which is exactly
|
|
152
|
+
// the journal position after applying the event.
|
|
153
|
+
cursor = event.id;
|
|
154
|
+
emit({ event, postCursor: cursor });
|
|
155
|
+
}
|
|
156
|
+
if (page.value.truncated) continue;
|
|
157
|
+
// An untruncated page was scanned to the journal head even when a
|
|
158
|
+
// collection filter emitted nothing from it: advance to that high-water
|
|
159
|
+
// mark so the next poll does not rescan the same tail. Emitted events keep
|
|
160
|
+
// their own postCursor; this only moves the internal resume point.
|
|
161
|
+
if (drained) {
|
|
162
|
+
cursor = maxCursor(cursor, page.value.latestCursor);
|
|
163
|
+
}
|
|
164
|
+
await sleep(pollIntervalMs, signal);
|
|
165
|
+
}
|
|
166
|
+
return { status: "stopped", cursor };
|
|
167
|
+
}
|
|
@@ -9,12 +9,14 @@ import type {
|
|
|
9
9
|
ListKnowledgeChangesInput,
|
|
10
10
|
} from "../../core/knowledge-delta";
|
|
11
11
|
import type { StorePort } from "../../store/types";
|
|
12
|
+
import type { ChangesFollowLine, FollowChangesResult } from "./changes-follow";
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
analyzeKnowledgeImpact,
|
|
15
16
|
getKnowledgeDiff,
|
|
16
17
|
listKnowledgeChanges,
|
|
17
18
|
} from "../../core/knowledge-delta";
|
|
19
|
+
import { followChanges, validateFollowCursor } from "./changes-follow";
|
|
18
20
|
import { initStore } from "./shared";
|
|
19
21
|
|
|
20
22
|
export interface KnowledgeDeltaCliContext {
|
|
@@ -82,6 +84,67 @@ export const impact = (
|
|
|
82
84
|
): Promise<KnowledgeDeltaServiceResult<KnowledgeImpactResult>> =>
|
|
83
85
|
withStore(context, (store) => impactRead(store, ref, input));
|
|
84
86
|
|
|
87
|
+
export interface ChangesFollowInput {
|
|
88
|
+
cursor?: string;
|
|
89
|
+
collection?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Abort on SIGINT/SIGTERM so the stream ends cleanly after the current line. */
|
|
93
|
+
const abortOnSignals = (): { signal: AbortSignal; dispose: () => void } => {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const onSignal = (): void => controller.abort();
|
|
96
|
+
process.once("SIGINT", onSignal);
|
|
97
|
+
process.once("SIGTERM", onSignal);
|
|
98
|
+
return {
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
dispose: (): void => {
|
|
101
|
+
process.off("SIGINT", onSignal);
|
|
102
|
+
process.off("SIGTERM", onSignal);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `gno changes --follow --jsonl`: stream journal events as JSON lines on
|
|
109
|
+
* stdout until a signal arrives or the resume cursor expires.
|
|
110
|
+
*/
|
|
111
|
+
export const changesFollow = async (
|
|
112
|
+
input: ChangesFollowInput,
|
|
113
|
+
context: KnowledgeDeltaCliContext = {},
|
|
114
|
+
emit: (line: ChangesFollowLine) => void = (line) => {
|
|
115
|
+
process.stdout.write(`${JSON.stringify(line)}\n`);
|
|
116
|
+
}
|
|
117
|
+
): Promise<FollowChangesResult> => {
|
|
118
|
+
if (input.cursor !== undefined) {
|
|
119
|
+
const invalid = validateFollowCursor(input.cursor);
|
|
120
|
+
if (invalid) return { status: "error", error: invalid, isValidation: true };
|
|
121
|
+
}
|
|
122
|
+
const collection = input.collection?.trim();
|
|
123
|
+
if (input.collection !== undefined && !collection) {
|
|
124
|
+
return {
|
|
125
|
+
status: "error",
|
|
126
|
+
error: "collection cannot be empty",
|
|
127
|
+
isValidation: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const signals = abortOnSignals();
|
|
131
|
+
try {
|
|
132
|
+
const result = await withStore(context, async (store) => ({
|
|
133
|
+
success: true as const,
|
|
134
|
+
data: await followChanges(
|
|
135
|
+
store,
|
|
136
|
+
{ cursor: input.cursor, collection, signal: signals.signal },
|
|
137
|
+
emit
|
|
138
|
+
),
|
|
139
|
+
}));
|
|
140
|
+
return result.success
|
|
141
|
+
? result.data
|
|
142
|
+
: { status: "error", error: result.error, isValidation: false };
|
|
143
|
+
} finally {
|
|
144
|
+
signals.dispose();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
85
148
|
const json = (value: unknown): string => JSON.stringify(value, null, 2);
|
|
86
149
|
|
|
87
150
|
export function formatChanges(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CollectionSyncResult } from "../../ingestion";
|
|
2
2
|
import type { HttpGatewayOverrides } from "../../mcp/http-security";
|
|
3
3
|
import type { BackgroundRuntimeResult } from "../../serve/background-runtime";
|
|
4
|
+
import type { FindingsPassResult } from "../../serve/findings-pass";
|
|
4
5
|
import type { ResidentRuntime } from "../../serve/resident-runtime";
|
|
5
6
|
|
|
6
7
|
import {
|
|
@@ -43,6 +44,29 @@ type DaemonDeps = {
|
|
|
43
44
|
logger?: DaemonLogger;
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
/** Silent when clean: only failures, and non-empty writes when not quiet, reach the log. */
|
|
48
|
+
export function logFindingsPassResult(
|
|
49
|
+
result: FindingsPassResult,
|
|
50
|
+
logger: DaemonLogger,
|
|
51
|
+
options: { quiet?: boolean; verbose?: boolean }
|
|
52
|
+
): void {
|
|
53
|
+
if (result.outcome === "failed") {
|
|
54
|
+
logger.error(`findings pass failed: ${result.error ?? "unknown error"}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (result.outcome === "skipped_lease") {
|
|
58
|
+
if (options.verbose) logger.log(`findings pass skipped: ${result.error}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const { counts } = result;
|
|
62
|
+
const changed =
|
|
63
|
+
counts.written + counts.reopened + counts.resolved + counts.deleted;
|
|
64
|
+
if (changed === 0 || options.quiet) return;
|
|
65
|
+
logger.log(
|
|
66
|
+
`findings pass: ${counts.written} new, ${counts.reopened} reopened, ${counts.resolved} resolved, ${counts.deleted} expired (${counts.open} open)`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
function formatCollectionSyncSummary(result: CollectionSyncResult): string {
|
|
47
71
|
return `${result.collection}: ${result.filesAdded} added, ${result.filesUpdated} updated, ${result.filesUnchanged} unchanged, ${result.filesErrored} errors`;
|
|
48
72
|
}
|
|
@@ -146,6 +170,11 @@ export async function daemon(
|
|
|
146
170
|
index: options.index,
|
|
147
171
|
requireCollections: true,
|
|
148
172
|
offline: options.offline,
|
|
173
|
+
onFindingsResult: (result) =>
|
|
174
|
+
logFindingsPassResult(result, logger, {
|
|
175
|
+
quiet: options.quiet,
|
|
176
|
+
verbose: options.verbose,
|
|
177
|
+
}),
|
|
149
178
|
watchCallbacks: {
|
|
150
179
|
onSyncStart: ({ collection, relPaths }) => {
|
|
151
180
|
if (!options.quiet) {
|
|
@@ -230,6 +259,12 @@ export async function daemon(
|
|
|
230
259
|
logger.error(`watch failed: ${failed.collection}: ${failed.reason}`);
|
|
231
260
|
}
|
|
232
261
|
}
|
|
262
|
+
const findings = (runtime as Partial<ResidentRuntime>).findingsScheduler;
|
|
263
|
+
if (findings) {
|
|
264
|
+
logger.log(
|
|
265
|
+
`findings pass: every ${findings.state.cadence} into "${findings.state.collection}" (report-only)`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
233
268
|
}
|
|
234
269
|
|
|
235
270
|
if (!options.noSyncOnStart) {
|
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
loadConfig,
|
|
22
22
|
} from "../../config";
|
|
23
23
|
import { isConnectorActivationComplete } from "../../core/activation-connector-health";
|
|
24
|
+
import {
|
|
25
|
+
findingsRunStatePathForIndex,
|
|
26
|
+
readFindingsRunStatus,
|
|
27
|
+
resolveFindingsSchedule,
|
|
28
|
+
} from "../../core/findings-run-state";
|
|
24
29
|
import { getCodeChunkingStatus } from "../../ingestion/chunker";
|
|
25
30
|
import { ModelCache } from "../../llm/cache";
|
|
26
31
|
import { getActivePreset, resolveModelUri } from "../../llm/registry";
|
|
@@ -560,6 +565,69 @@ async function checkSqliteExtensions(): Promise<DoctorCheck[]> {
|
|
|
560
565
|
return checks;
|
|
561
566
|
}
|
|
562
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Report the daemon's scheduled findings pass from its persisted state so a
|
|
570
|
+
* misconfigured, starved, or failing scheduler is visible without the daemon.
|
|
571
|
+
*/
|
|
572
|
+
export async function checkFindingsPass(
|
|
573
|
+
config: Config,
|
|
574
|
+
indexName?: string
|
|
575
|
+
): Promise<DoctorCheck> {
|
|
576
|
+
const name = "findings-pass";
|
|
577
|
+
const resolution = resolveFindingsSchedule(config);
|
|
578
|
+
if (!resolution.ok) {
|
|
579
|
+
return {
|
|
580
|
+
name,
|
|
581
|
+
status: "error",
|
|
582
|
+
message: "misconfigured",
|
|
583
|
+
details: [resolution.error],
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (!resolution.enabled) {
|
|
587
|
+
return {
|
|
588
|
+
name,
|
|
589
|
+
status: "ok",
|
|
590
|
+
message: "disabled (opt-in via findings.enabled)",
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const status = await readFindingsRunStatus(
|
|
594
|
+
findingsRunStatePathForIndex(indexName)
|
|
595
|
+
);
|
|
596
|
+
const schedule = `every ${resolution.schedule.cadence} into "${resolution.schedule.collection.name}"`;
|
|
597
|
+
if (!status) {
|
|
598
|
+
return {
|
|
599
|
+
name,
|
|
600
|
+
status: "warn",
|
|
601
|
+
message: `enabled (${schedule}) but no run state recorded`,
|
|
602
|
+
details: ["Start gno daemon; the pass only runs inside the daemon."],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const details = [
|
|
606
|
+
`last run: ${status.lastRunAt ?? "never"}`,
|
|
607
|
+
`last success: ${status.lastSuccessAt ?? "never"}`,
|
|
608
|
+
`next due: ${status.nextDueAt}`,
|
|
609
|
+
];
|
|
610
|
+
if (status.counts) {
|
|
611
|
+
details.push(
|
|
612
|
+
`counts: ${status.counts.open} open, ${status.counts.written} new, ${status.counts.resolved} resolved`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
if (status.error) details.push(`error: ${status.error}`);
|
|
616
|
+
const statusOf: Record<typeof status.state, DoctorCheckStatus> = {
|
|
617
|
+
pending: "ok",
|
|
618
|
+
success: "ok",
|
|
619
|
+
skipped_lease: "warn",
|
|
620
|
+
overdue: "warn",
|
|
621
|
+
failed: "error",
|
|
622
|
+
};
|
|
623
|
+
return {
|
|
624
|
+
name,
|
|
625
|
+
status: statusOf[status.state],
|
|
626
|
+
message: `${status.state} (${schedule})`,
|
|
627
|
+
details,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
563
631
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
564
632
|
// Implementation
|
|
565
633
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -603,6 +671,9 @@ export async function doctor(
|
|
|
603
671
|
// Embedding fingerprint freshness
|
|
604
672
|
checks.push(await checkEmbeddingFingerprints(config, options.indexName));
|
|
605
673
|
|
|
674
|
+
// Scheduled findings pass (daemon-only, opt-in)
|
|
675
|
+
checks.push(await checkFindingsPass(config, options.indexName));
|
|
676
|
+
|
|
606
677
|
const activation = await buildDoctorActivation(config, options);
|
|
607
678
|
checks.push(checkRetrievalActivation(activation));
|
|
608
679
|
const connectorActivation = checkConnectorActivation(activation);
|