kodingo-cli 1.1.13 → 1.3.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/dist/adapters/cloud-adapter/cloud-persistence.js +2 -0
- package/dist/adapters/db-adapter/memory-persistence.js +40 -11
- package/dist/cli.js +15 -1
- package/dist/commands/build.js +19 -3
- package/dist/commands/capture.js +6 -0
- package/dist/commands/deny.js +4 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/plan.js +210 -0
- package/dist/commands/scan-claude.js +1 -0
- package/dist/commands/scan-repo.js +4 -0
- package/dist/ports/cloud-event-port.js +2 -0
- package/dist/ports/db-event-port.js +5 -1
- package/package.json +3 -3
|
@@ -104,6 +104,7 @@ function mapApiRecord(r) {
|
|
|
104
104
|
},
|
|
105
105
|
createdAt: new Date(r.createdAt),
|
|
106
106
|
updatedAt: new Date(r.updatedAt),
|
|
107
|
+
evidenceKind: r.evidenceKind ?? "sourced",
|
|
107
108
|
// Adapter-layer fields — not on core MemoryRecord but present in API responses
|
|
108
109
|
externalId: r.externalId ?? null,
|
|
109
110
|
correctsId: r.correctsId ?? null,
|
|
@@ -146,6 +147,7 @@ async function saveMemory(input) {
|
|
|
146
147
|
status: input.status ?? "proposed",
|
|
147
148
|
confidence: input.confidence ?? 0.3,
|
|
148
149
|
tags: input.tags ?? [],
|
|
150
|
+
evidenceKind: input.evidenceKind,
|
|
149
151
|
};
|
|
150
152
|
if (input.title)
|
|
151
153
|
body.title = input.title;
|
|
@@ -46,6 +46,21 @@ async function initDb() {
|
|
|
46
46
|
ALTER TABLE kodingo_memory ADD COLUMN IF NOT EXISTS corrects_id uuid NULL;
|
|
47
47
|
ALTER TABLE kodingo_memory ADD COLUMN IF NOT EXISTS corrected_by_id uuid NULL;
|
|
48
48
|
|
|
49
|
+
-- Real evidence provenance (mirrors kodingo-api's schema.ts) — local-mode
|
|
50
|
+
-- writes go straight here via psql, bypassing the API's enforcement
|
|
51
|
+
-- entirely, so the same real constraint needs to exist on this table too.
|
|
52
|
+
ALTER TABLE kodingo_memory ADD COLUMN IF NOT EXISTS evidence_kind text NOT NULL DEFAULT 'sourced';
|
|
53
|
+
DO $$
|
|
54
|
+
BEGIN
|
|
55
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'kodingo_memory_evidence_kind_check') THEN
|
|
56
|
+
ALTER TABLE kodingo_memory ADD CONSTRAINT kodingo_memory_evidence_kind_check CHECK (evidence_kind IN ('sourced', 'derived'));
|
|
57
|
+
END IF;
|
|
58
|
+
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'kodingo_memory_sourced_requires_external_id') THEN
|
|
59
|
+
ALTER TABLE kodingo_memory ADD CONSTRAINT kodingo_memory_sourced_requires_external_id
|
|
60
|
+
CHECK (evidence_kind = 'derived' OR (external_id IS NOT NULL AND btrim(external_id) <> '')) NOT VALID;
|
|
61
|
+
END IF;
|
|
62
|
+
END $$;
|
|
63
|
+
|
|
49
64
|
-- Indexes
|
|
50
65
|
CREATE INDEX IF NOT EXISTS kodingo_memory_created_at_idx
|
|
51
66
|
ON kodingo_memory (created_at);
|
|
@@ -97,11 +112,23 @@ async function saveMemory(input) {
|
|
|
97
112
|
const id = (0, uuid_1.v4)();
|
|
98
113
|
const status = input.status ?? "proposed";
|
|
99
114
|
const confidence = clamp01(input.confidence ?? 0.3);
|
|
115
|
+
// Real mechanical enforcement (mirrors kodingo-core's
|
|
116
|
+
// validateMemoryProvenance) — local-mode writes go straight to Postgres
|
|
117
|
+
// via psql, bypassing kodingo-api's routes/memory.ts entirely, so this is
|
|
118
|
+
// the one place that enforcement can actually happen for this path. Same
|
|
119
|
+
// real, honest fallback as the API: a "sourced" capture with nothing
|
|
120
|
+
// client-supplied gets a real identifier naming the capture event itself,
|
|
121
|
+
// not a fabricated placeholder.
|
|
122
|
+
const externalId = input.externalId ??
|
|
123
|
+
(input.evidenceKind === "sourced" ? `manual:${input.repoPath ?? "unknown"}:${id}` : undefined);
|
|
124
|
+
if (input.evidenceKind === "sourced" && !externalId?.trim()) {
|
|
125
|
+
throw new Error("A 'sourced' memory must include a real externalId. If this genuinely has no single real source, use evidenceKind: 'derived' instead.");
|
|
126
|
+
}
|
|
100
127
|
const sql = `
|
|
101
128
|
WITH inserted AS (
|
|
102
129
|
INSERT INTO kodingo_memory (
|
|
103
130
|
id, type, title, content, tags, repo_path,
|
|
104
|
-
status, confidence, symbol, external_id,
|
|
131
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
105
132
|
corrects_id, corrected_by_id
|
|
106
133
|
)
|
|
107
134
|
VALUES (
|
|
@@ -114,13 +141,14 @@ async function saveMemory(input) {
|
|
|
114
141
|
${sqlLiteral(status)},
|
|
115
142
|
${sqlNumberLiteral(confidence)},
|
|
116
143
|
${sqlNullableLiteral(input.symbol)},
|
|
117
|
-
${sqlNullableLiteral(
|
|
144
|
+
${sqlNullableLiteral(externalId)},
|
|
145
|
+
${sqlLiteral(input.evidenceKind)},
|
|
118
146
|
${sqlNullableUuidLiteral(input.correctsId)},
|
|
119
147
|
${sqlNullableUuidLiteral(input.correctedById)}
|
|
120
148
|
)
|
|
121
149
|
RETURNING
|
|
122
150
|
id, type, title, content, tags, repo_path,
|
|
123
|
-
status, confidence, symbol, external_id,
|
|
151
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
124
152
|
corrects_id, corrected_by_id,
|
|
125
153
|
created_at
|
|
126
154
|
)
|
|
@@ -137,7 +165,7 @@ async function getMemoryById(id) {
|
|
|
137
165
|
SELECT row_to_json(result) FROM (
|
|
138
166
|
SELECT
|
|
139
167
|
id, type, title, content, tags, repo_path,
|
|
140
|
-
status, confidence, symbol, external_id,
|
|
168
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
141
169
|
corrects_id, corrected_by_id,
|
|
142
170
|
created_at
|
|
143
171
|
FROM kodingo_memory
|
|
@@ -165,7 +193,7 @@ async function updateMemoryLifecycle(params) {
|
|
|
165
193
|
WHERE id = ${sqlLiteral(params.id)}
|
|
166
194
|
RETURNING
|
|
167
195
|
id, type, title, content, tags, repo_path,
|
|
168
|
-
status, confidence, symbol, external_id,
|
|
196
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
169
197
|
corrects_id, corrected_by_id,
|
|
170
198
|
created_at
|
|
171
199
|
)
|
|
@@ -218,7 +246,7 @@ async function getMemoryByExternalId(externalId, repoPath) {
|
|
|
218
246
|
SELECT row_to_json(result) FROM (
|
|
219
247
|
SELECT
|
|
220
248
|
id, type, title, content, tags, repo_path,
|
|
221
|
-
status, confidence, symbol, external_id,
|
|
249
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
222
250
|
corrects_id, corrected_by_id,
|
|
223
251
|
created_at
|
|
224
252
|
FROM kodingo_memory
|
|
@@ -243,7 +271,7 @@ async function getProposedOrIgnoredRecordBySymbol(params) {
|
|
|
243
271
|
SELECT row_to_json(result) FROM (
|
|
244
272
|
SELECT
|
|
245
273
|
id, type, title, content, tags, repo_path,
|
|
246
|
-
status, confidence, symbol, external_id,
|
|
274
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
247
275
|
corrects_id, corrected_by_id,
|
|
248
276
|
created_at
|
|
249
277
|
FROM kodingo_memory
|
|
@@ -275,7 +303,7 @@ async function updateMemoryConfidence(params) {
|
|
|
275
303
|
WHERE id = ${sqlLiteral(params.id)}
|
|
276
304
|
RETURNING
|
|
277
305
|
id, type, title, content, tags, repo_path,
|
|
278
|
-
status, confidence, symbol, external_id,
|
|
306
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
279
307
|
corrects_id, corrected_by_id,
|
|
280
308
|
created_at
|
|
281
309
|
)
|
|
@@ -318,7 +346,7 @@ async function queryMemory(text, limit = 10, repoPath, opts = {}) {
|
|
|
318
346
|
SELECT row_to_json(result) FROM (
|
|
319
347
|
SELECT DISTINCT ON (coalesce(symbol, id::text))
|
|
320
348
|
id, type, title, content, tags, repo_path,
|
|
321
|
-
status, confidence, symbol, external_id,
|
|
349
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
322
350
|
corrects_id, corrected_by_id,
|
|
323
351
|
created_at
|
|
324
352
|
FROM kodingo_memory
|
|
@@ -349,7 +377,7 @@ async function queryMemory(text, limit = 10, repoPath, opts = {}) {
|
|
|
349
377
|
SELECT row_to_json(result) FROM (
|
|
350
378
|
SELECT
|
|
351
379
|
id, type, title, content, tags, repo_path,
|
|
352
|
-
status, confidence, symbol, external_id,
|
|
380
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
353
381
|
corrects_id, corrected_by_id,
|
|
354
382
|
created_at,
|
|
355
383
|
CASE
|
|
@@ -413,7 +441,7 @@ async function queryMemoryBySymbol(params) {
|
|
|
413
441
|
SELECT row_to_json(result) FROM (
|
|
414
442
|
SELECT
|
|
415
443
|
id, type, title, content, tags, repo_path,
|
|
416
|
-
status, confidence, symbol, external_id,
|
|
444
|
+
status, confidence, symbol, external_id, evidence_kind,
|
|
417
445
|
corrects_id, corrected_by_id,
|
|
418
446
|
created_at
|
|
419
447
|
FROM kodingo_memory
|
|
@@ -444,6 +472,7 @@ function mapRow(row) {
|
|
|
444
472
|
confidence,
|
|
445
473
|
symbol: row.symbol ?? null,
|
|
446
474
|
externalId: row.external_id ?? null,
|
|
475
|
+
evidenceKind: row.evidence_kind ?? "sourced",
|
|
447
476
|
correctsId: row.corrects_id ?? null,
|
|
448
477
|
correctedById: row.corrected_by_id ?? null,
|
|
449
478
|
createdAt: new Date(row.created_at),
|
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ const sync_dependencies_1 = require("./commands/sync-dependencies");
|
|
|
19
19
|
const security_scan_1 = require("./commands/security-scan");
|
|
20
20
|
const init_1 = require("./commands/init");
|
|
21
21
|
const build_1 = require("./commands/build");
|
|
22
|
+
const plan_1 = require("./commands/plan");
|
|
22
23
|
const install_hook_1 = require("./commands/install-hook");
|
|
23
24
|
const update_1 = require("./commands/update");
|
|
24
25
|
const sync_1 = require("./commands/sync");
|
|
@@ -39,7 +40,7 @@ program
|
|
|
39
40
|
.argument("<task>", "what to build or fix")
|
|
40
41
|
.option("--repo <path>", "repo path (defaults to the current repo)")
|
|
41
42
|
.option("--max-turns <n>", "override the agent loop's turn budget")
|
|
42
|
-
.option("--scope <small|substantial>", "manual scope estimate sizing the initial token budget
|
|
43
|
+
.option("--scope <small|substantial>", "manual scope estimate sizing the initial token budget — overrides the scope from a prior `kodingo plan` run against this repo, if any")
|
|
43
44
|
.action(async (task, options) => {
|
|
44
45
|
if (options.scope && options.scope !== "small" && options.scope !== "substantial") {
|
|
45
46
|
console.error(`✖ --scope must be "small" or "substantial", got "${options.scope}".`);
|
|
@@ -52,6 +53,19 @@ program
|
|
|
52
53
|
...(options.scope ? { scope: options.scope } : {}),
|
|
53
54
|
});
|
|
54
55
|
});
|
|
56
|
+
// ── plan ──────────────────────────────────────────────────────────────────────
|
|
57
|
+
program
|
|
58
|
+
.command("plan")
|
|
59
|
+
.description("Run Kortex Plan's local, observation-only agent loop against this repo (test-driver for kodingo-agent)")
|
|
60
|
+
.argument("<task>", "what to plan")
|
|
61
|
+
.option("--repo <path>", "repo path (defaults to the current repo)")
|
|
62
|
+
.option("--max-turns <n>", "override the agent loop's turn budget")
|
|
63
|
+
.action(async (task, options) => {
|
|
64
|
+
await (0, plan_1.planCommand)(String(task), {
|
|
65
|
+
...(options.repo ? { repo: String(options.repo) } : {}),
|
|
66
|
+
...(options.maxTurns ? { maxTurns: Number.parseInt(String(options.maxTurns), 10) } : {}),
|
|
67
|
+
});
|
|
68
|
+
});
|
|
55
69
|
// ── update ────────────────────────────────────────────────────────────────────
|
|
56
70
|
program
|
|
57
71
|
.command("update")
|
package/dist/commands/build.js
CHANGED
|
@@ -118,6 +118,18 @@ async function buildCommand(input, options = {}) {
|
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
120
|
const apiBaseUrl = apiConfig.apiUrl.replace(/\/$/, "");
|
|
121
|
+
// A real `kodingo plan` run against this same repo left a one-shot scope
|
|
122
|
+
// estimate behind — use it unless a human explicitly passed --scope,
|
|
123
|
+
// which always wins. Consumed (cleared) here so a stale plan from a
|
|
124
|
+
// different task never silently sizes a later, unrelated build.
|
|
125
|
+
let scope = options.scope;
|
|
126
|
+
const existingWs = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
|
|
127
|
+
if (!scope && existingWs?.lastPlan) {
|
|
128
|
+
scope = existingWs.lastPlan.scope;
|
|
129
|
+
console.log(`Using scope:${scope} from the last \`kodingo plan\` run against this repo (${existingWs.lastPlan.createdAt}).`);
|
|
130
|
+
const { lastPlan: _consumed, ...rest } = existingWs;
|
|
131
|
+
(0, persistence_config_1.writeWorkspaceConfig)(repoRoot, rest);
|
|
132
|
+
}
|
|
121
133
|
// Bring-your-own, if configured (~/.kodingo/byo-model.json — same file
|
|
122
134
|
// kodingo-vscode already uses) — never logged, only forwarded.
|
|
123
135
|
const byoModelConfig = (0, kodingo_agent_1.getByoModelConfig)();
|
|
@@ -150,13 +162,17 @@ async function buildCommand(input, options = {}) {
|
|
|
150
162
|
"Explore before you edit: read files, grep for symbols, and look around as needed — looking is just " +
|
|
151
163
|
"another turn, you can call tools as many times as it takes to actually understand the real code " +
|
|
152
164
|
"before changing it. Only use edit_file for surgical, minimal changes to existing files; use " +
|
|
153
|
-
"write_file only for brand-new files. Before reporting done, verify your change with run_command."
|
|
165
|
+
"write_file only for brand-new files. Before reporting done, verify your change with run_command. " +
|
|
166
|
+
"If you discover mid-task that a prior plan, assumption, or existing memory was meaningfully wrong — " +
|
|
167
|
+
"a file assumed to exist that doesn't, an existing pattern that's actually different from what was " +
|
|
168
|
+
"documented — call propose_memory_correction to record it for human review. Only for real, meaningful " +
|
|
169
|
+
"corrections, not routine exploration findings.",
|
|
154
170
|
},
|
|
155
171
|
{ role: "user", content: input },
|
|
156
172
|
];
|
|
157
173
|
const result = await (0, kodingo_agent_1.runAgentLoop)({
|
|
158
174
|
cwd: repoRoot,
|
|
159
|
-
tools: (0, kodingo_agent_1.createDefaultToolSet)(),
|
|
175
|
+
tools: (0, kodingo_agent_1.createDefaultToolSet)({ memoryCorrection: { apiBaseUrl, projectToken: apiConfig.token } }),
|
|
160
176
|
initialMessages,
|
|
161
177
|
modelPort,
|
|
162
178
|
reporter: (0, kodingo_agent_1.createTerminalReporter)(),
|
|
@@ -164,7 +180,7 @@ async function buildCommand(input, options = {}) {
|
|
|
164
180
|
// not a silent hard kill. See kodingo-agent's terminalCheckpointPort.
|
|
165
181
|
checkpointPort: (0, kodingo_agent_1.createTerminalCheckpointPort)(),
|
|
166
182
|
budgets: {
|
|
167
|
-
...(0, kodingo_agent_1.resolveBudgetForScope)(
|
|
183
|
+
...(0, kodingo_agent_1.resolveBudgetForScope)(scope),
|
|
168
184
|
...(options.maxTurns ? { maxTurns: options.maxTurns } : {}),
|
|
169
185
|
},
|
|
170
186
|
});
|
package/dist/commands/capture.js
CHANGED
|
@@ -17,10 +17,16 @@ async function captureCommand(options) {
|
|
|
17
17
|
}
|
|
18
18
|
const tags = parseTags(options.tags);
|
|
19
19
|
const repoPath = findRepoRoot(process.cwd());
|
|
20
|
+
// A manual capture is genuinely sourced — to the real, specific event of
|
|
21
|
+
// this human's assertion, right now. Not tied to a pre-existing external
|
|
22
|
+
// artifact the way a commit or PR is, so no externalId is set here; both
|
|
23
|
+
// backends (local-mode saveMemory, kodingo-api's routes/memory.ts)
|
|
24
|
+
// construct a real one from the capture event's own identity when absent.
|
|
20
25
|
const input = {
|
|
21
26
|
type: options.type,
|
|
22
27
|
content,
|
|
23
28
|
repoPath,
|
|
29
|
+
evidenceKind: "sourced",
|
|
24
30
|
};
|
|
25
31
|
if (options.title)
|
|
26
32
|
input.title = options.title;
|
package/dist/commands/deny.js
CHANGED
|
@@ -37,12 +37,16 @@ async function denyCommand(options) {
|
|
|
37
37
|
}
|
|
38
38
|
const userTags = parseTags(options.tags);
|
|
39
39
|
const tags = mergeTags(existing.tags, userTags);
|
|
40
|
+
// A denial's corrected replacement is a new, real human assertion — the
|
|
41
|
+
// correction event itself — same class as manual capture, not simply
|
|
42
|
+
// inherited from whatever the original record's provenance was.
|
|
40
43
|
const input = {
|
|
41
44
|
type: existing.type,
|
|
42
45
|
content: correctionContent,
|
|
43
46
|
status: "proposed",
|
|
44
47
|
confidence: 0.7,
|
|
45
48
|
correctsId: existing.id,
|
|
49
|
+
evidenceKind: "sourced",
|
|
46
50
|
};
|
|
47
51
|
if (existing.repo)
|
|
48
52
|
input.repoPath = existing.repo;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* kodingo plan
|
|
4
|
+
*
|
|
5
|
+
* Runs Kortex Plan's real local agentic loop (kodingo-agent) against the
|
|
6
|
+
* current repo — observation only (read_file, grep, list_directory, glob,
|
|
7
|
+
* query_memory), never edit_file/write_file/run_command. Same real
|
|
8
|
+
* test-driver identity as `kodingo build` (see build.ts's own header
|
|
9
|
+
* comment): this exercises kodingo-agent's Plan tool set headlessly, not a
|
|
10
|
+
* polished second UX.
|
|
11
|
+
*
|
|
12
|
+
* On success, the real structured output (scope, filesLikelyInvolved,
|
|
13
|
+
* openQuestions) is written to this repo's workspace config as a one-shot
|
|
14
|
+
* handoff — `kodingo build` reads and consumes it to size its own budget,
|
|
15
|
+
* so a plain `kodingo build "<task>"` run after a `kodingo plan` run for
|
|
16
|
+
* the same repo picks up a real scope estimate without a manual --scope flag.
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.planCommand = planCommand;
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const kodingo_agent_1 = require("kodingo-agent");
|
|
56
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
57
|
+
function findRepoRoot(startPath) {
|
|
58
|
+
let current = startPath;
|
|
59
|
+
while (true) {
|
|
60
|
+
if (fs.existsSync(path.join(current, ".git")))
|
|
61
|
+
return current;
|
|
62
|
+
const parent = path.dirname(current);
|
|
63
|
+
if (parent === current)
|
|
64
|
+
return startPath;
|
|
65
|
+
current = parent;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function resolveApiConfig(repoRoot) {
|
|
69
|
+
const config = (0, persistence_config_1.readConfig)();
|
|
70
|
+
const ws = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
|
|
71
|
+
const apiUrl = ws?.apiUrl ?? config.apiUrl;
|
|
72
|
+
const token = ws?.token ?? config.token;
|
|
73
|
+
if (!apiUrl || !token)
|
|
74
|
+
return null;
|
|
75
|
+
return { apiUrl, token };
|
|
76
|
+
}
|
|
77
|
+
async function planCommand(input, options = {}) {
|
|
78
|
+
const repoRoot = options.repo ? path.resolve(options.repo) : findRepoRoot(process.cwd());
|
|
79
|
+
const apiConfig = resolveApiConfig(repoRoot);
|
|
80
|
+
if (!apiConfig) {
|
|
81
|
+
console.error(`✖ No Kodingo project connected for ${repoRoot}. Run \`kodingo init\` in this repo first.`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// Plan only ever reads — the trust prompt exists for Build's write/execute
|
|
86
|
+
// capability, so Plan doesn't need to ask for it, but it still respects an
|
|
87
|
+
// existing "always allow" grant by not double-prompting within it either way.
|
|
88
|
+
if (!(0, kodingo_agent_1.isProjectTrusted)(repoRoot)) {
|
|
89
|
+
console.log(`Note: ${repoRoot} isn't marked trusted yet — Plan only reads, so this runs regardless, but \`kodingo build\` will still ask.`);
|
|
90
|
+
}
|
|
91
|
+
const apiBaseUrl = apiConfig.apiUrl.replace(/\/$/, "");
|
|
92
|
+
const byoModelConfig = (0, kodingo_agent_1.getByoModelConfig)();
|
|
93
|
+
if (byoModelConfig) {
|
|
94
|
+
console.log(`Using bring-your-own model${byoModelConfig.label ? ` (${byoModelConfig.label})` : ""} — no shared quota consumed.`);
|
|
95
|
+
}
|
|
96
|
+
console.log(`Starting Kortex Plan against ${repoRoot} ...`);
|
|
97
|
+
const startRes = await fetch(`${apiBaseUrl}/build/agent-start`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": apiConfig.token },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
input,
|
|
102
|
+
repo: path.basename(repoRoot),
|
|
103
|
+
mode: "plan",
|
|
104
|
+
...(byoModelConfig ? { modelConfig: byoModelConfig } : {}),
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
const startBody = (await startRes.json());
|
|
108
|
+
if (!startRes.ok || !startBody.sessionId) {
|
|
109
|
+
console.error(`✖ Could not start Plan session: ${startBody.error ?? startRes.status}`);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const sessionId = startBody.sessionId;
|
|
114
|
+
console.log(`Session: ${sessionId}`);
|
|
115
|
+
const modelPort = (0, kodingo_agent_1.createHttpModelCallPort)({ apiBaseUrl, projectToken: apiConfig.token, sessionId });
|
|
116
|
+
const initialMessages = [
|
|
117
|
+
{
|
|
118
|
+
role: "system",
|
|
119
|
+
content: "You are Kortex Plan, working directly against the developer's real local repository. Your job is to " +
|
|
120
|
+
"produce a real, grounded plan for the task below — not a guess. You have NO ability to edit files or " +
|
|
121
|
+
"run commands — observation only.\n\n" +
|
|
122
|
+
"Explore in this order, narrow before wide, memory before raw content — this is enforced, not just " +
|
|
123
|
+
"advice: read_file will be mechanically rejected unless you've already either (a) called query_memory " +
|
|
124
|
+
"with a query naming that file, or (b) called list_directory/glob so it actually appears in the results. " +
|
|
125
|
+
"1) Start from query_memory with a question shaped by the task itself (e.g. 'how does rate limiting work " +
|
|
126
|
+
"in this codebase'). If the answer is genuinely sufficient, reason from it directly — a well-maintained " +
|
|
127
|
+
"project should let you produce a grounded plan with little or no raw file reading at all; that's the " +
|
|
128
|
+
"point, not a shortcut you're avoiding. 2) If insufficient, either ask a different, more specific memory " +
|
|
129
|
+
"question, or fall back to real exploration — that fallback is legitimate and expected for anything " +
|
|
130
|
+
"memory doesn't cover yet. 3) When falling back, start with list_directory/glob — file names and " +
|
|
131
|
+
"structure only, never full contents at this stage. 4) Once a specific file looks relevant, query memory " +
|
|
132
|
+
"again scoped to that file ('what do we know about authRoutes.js') before reading it — memory may cover " +
|
|
133
|
+
"it even if your broader query didn't. 5) Only then read_file, as a genuine last resort.\n\n" +
|
|
134
|
+
"If something is genuinely ambiguous and you can't proceed confidently without knowing the answer, use " +
|
|
135
|
+
"ask_clarifying_question — sparingly, only for real blockers, not things you could resolve yourself by " +
|
|
136
|
+
"reading more code. Before calling submit_plan, double-check your own draft against the real codebase: " +
|
|
137
|
+
"re-verify that every file path you're about to list under filesLikelyInvolved actually exists and is " +
|
|
138
|
+
"really relevant — submit_plan will mechanically reject any path that doesn't really exist. Give an " +
|
|
139
|
+
"honest scope estimate: 'small' for a narrow, single-file change; 'substantial' for a real multi-file " +
|
|
140
|
+
"feature. Your planMarkdown has two real destinations — sent straight to Build, or downloaded and taken " +
|
|
141
|
+
"somewhere else entirely with zero further involvement from Kortex — so write it as a genuinely complete, " +
|
|
142
|
+
"standalone document: real file paths, specific locations within them, real reasoning for each change.",
|
|
143
|
+
},
|
|
144
|
+
{ role: "user", content: input },
|
|
145
|
+
];
|
|
146
|
+
const { tools, drainBackgroundWork } = (0, kodingo_agent_1.createPlanToolSet)({
|
|
147
|
+
queryMemory: { apiBaseUrl, projectToken: apiConfig.token },
|
|
148
|
+
modelPort,
|
|
149
|
+
clarificationPort: (0, kodingo_agent_1.createTerminalClarificationPort)(),
|
|
150
|
+
});
|
|
151
|
+
const result = await (0, kodingo_agent_1.runAgentLoop)({
|
|
152
|
+
cwd: repoRoot,
|
|
153
|
+
tools,
|
|
154
|
+
initialMessages,
|
|
155
|
+
modelPort,
|
|
156
|
+
reporter: (0, kodingo_agent_1.createTerminalReporter)(),
|
|
157
|
+
checkpointPort: (0, kodingo_agent_1.createTerminalCheckpointPort)(),
|
|
158
|
+
drainBackgroundWork,
|
|
159
|
+
// Plan's whole job is thorough exploration BEFORE any task-specific
|
|
160
|
+
// scope is known, so it needs real headroom by default, not the flat
|
|
161
|
+
// 40-turn default — confirmed directly: a real Plan run against Pulse
|
|
162
|
+
// hit budget_exceeded at exactly 40 turns of pure reading (689,805
|
|
163
|
+
// tokens, well under the token budget) without ever reaching
|
|
164
|
+
// submit_plan. Using the same "substantial" floor Build gets for a
|
|
165
|
+
// real multi-file task, not the bare default.
|
|
166
|
+
budgets: {
|
|
167
|
+
...(0, kodingo_agent_1.resolveBudgetForScope)("substantial"),
|
|
168
|
+
...(options.maxTurns ? { maxTurns: options.maxTurns } : {}),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
await fetch(`${apiBaseUrl}/build/${sessionId}/agent-complete`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": apiConfig.token },
|
|
175
|
+
body: JSON.stringify({ status: result.status, message: result.error }),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Best-effort — the loop's own outcome already printed to the terminal regardless.
|
|
180
|
+
}
|
|
181
|
+
if (result.status !== "completed" || !result.structuredOutput) {
|
|
182
|
+
console.error(`✖ Plan did not complete: ${result.status}${result.error ? ` — ${result.error}` : ""}`);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const output = result.structuredOutput;
|
|
187
|
+
console.log(`\n${output.planMarkdown}\n`);
|
|
188
|
+
console.log(`Scope: ${output.scope}`);
|
|
189
|
+
console.log(`Files likely involved: ${output.filesLikelyInvolved.join(", ") || "(none)"}`);
|
|
190
|
+
if (output.openQuestions.length > 0) {
|
|
191
|
+
console.log(`Open questions:\n${output.openQuestions.map((q) => ` - ${q}`).join("\n")}`);
|
|
192
|
+
}
|
|
193
|
+
// One-shot handoff: kodingo build reads and clears this on its next run
|
|
194
|
+
// against the same repo, sizing its own budget without a manual --scope.
|
|
195
|
+
const existingWs = (0, persistence_config_1.readWorkspaceConfig)(repoRoot);
|
|
196
|
+
(0, persistence_config_1.writeWorkspaceConfig)(repoRoot, {
|
|
197
|
+
mode: "cloud",
|
|
198
|
+
apiUrl: apiConfig.apiUrl,
|
|
199
|
+
token: apiConfig.token,
|
|
200
|
+
...existingWs,
|
|
201
|
+
lastPlan: {
|
|
202
|
+
scope: output.scope,
|
|
203
|
+
planMarkdown: output.planMarkdown,
|
|
204
|
+
filesLikelyInvolved: output.filesLikelyInvolved,
|
|
205
|
+
openQuestions: output.openQuestions,
|
|
206
|
+
createdAt: new Date().toISOString(),
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
console.log(`\n✔ Saved as this repo's last plan — \`kodingo build\` will use scope:${output.scope} automatically on its next run here.`);
|
|
210
|
+
}
|
|
@@ -104,6 +104,7 @@ function extractMemoriesFromSession(messages, sessionId, repoPath) {
|
|
|
104
104
|
repoPath,
|
|
105
105
|
status: "proposed",
|
|
106
106
|
confidence: Math.min(0.3 + score * 0.05, 0.6),
|
|
107
|
+
evidenceKind: "sourced",
|
|
107
108
|
externalId: `claude-session:${sessionId}:${Buffer.from(para.slice(0, 40)).toString("base64").slice(0, 16)}`,
|
|
108
109
|
});
|
|
109
110
|
}
|
|
@@ -197,6 +197,10 @@ async function inferAndCapture(token, symbol, repo, graph, repoRoot) {
|
|
|
197
197
|
machineId: (0, persistence_config_1.readConfig)().machineId ?? undefined,
|
|
198
198
|
displayName: os.hostname(),
|
|
199
199
|
fingerprint: symbol.fingerprint ?? undefined,
|
|
200
|
+
// An LLM's own inferred description of a function during a bulk scan
|
|
201
|
+
// — Kortex's own conclusion, not traceable to one real external
|
|
202
|
+
// event, same class as kodingo-api's githubScanner.ts.
|
|
203
|
+
evidenceKind: "derived",
|
|
200
204
|
}),
|
|
201
205
|
});
|
|
202
206
|
return saveRes.ok;
|
|
@@ -58,6 +58,7 @@ class CloudEventPort {
|
|
|
58
58
|
content: existing.content,
|
|
59
59
|
symbol: existing.symbol ?? "",
|
|
60
60
|
tags: existing.tags ?? [],
|
|
61
|
+
evidenceKind: existing.evidenceKind,
|
|
61
62
|
status: existing.status,
|
|
62
63
|
confidence: existing.confidence,
|
|
63
64
|
evidence: {
|
|
@@ -100,6 +101,7 @@ class CloudEventPort {
|
|
|
100
101
|
repoPath: this.repoPath,
|
|
101
102
|
status: "proposed",
|
|
102
103
|
confidence: decision.confidence ?? 0.3,
|
|
104
|
+
evidenceKind: "sourced",
|
|
103
105
|
externalId,
|
|
104
106
|
tags: ["signal:git", "kind:decision"],
|
|
105
107
|
};
|
|
@@ -52,6 +52,7 @@ class DbEventPort {
|
|
|
52
52
|
content: existing.content,
|
|
53
53
|
symbol: existing.symbol ?? "",
|
|
54
54
|
tags: existing.tags ?? [],
|
|
55
|
+
evidenceKind: existing.evidenceKind,
|
|
55
56
|
status: existing.status,
|
|
56
57
|
confidence: existing.confidence,
|
|
57
58
|
evidence: {
|
|
@@ -74,13 +75,16 @@ class DbEventPort {
|
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
|
-
// No existing proposed/ignored record — insert new record as usual
|
|
78
|
+
// No existing proposed/ignored record — insert new record as usual.
|
|
79
|
+
// Git-signal-derived captures are genuinely sourced — externalId here is
|
|
80
|
+
// a real git:<commitHash> reference (see the caller).
|
|
78
81
|
const input = {
|
|
79
82
|
type: "decision",
|
|
80
83
|
content,
|
|
81
84
|
repoPath: this.repoPath,
|
|
82
85
|
status: "proposed",
|
|
83
86
|
confidence: 0.3,
|
|
87
|
+
evidenceKind: "sourced",
|
|
84
88
|
externalId,
|
|
85
89
|
};
|
|
86
90
|
if (title)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodingo-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Kodingo CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@typescript-eslint/types": "^8.61.1",
|
|
26
26
|
"@typescript-eslint/typescript-estree": "^8.61.1",
|
|
27
27
|
"commander": "^12.1.0",
|
|
28
|
-
"kodingo-agent": "^0.
|
|
29
|
-
"kodingo-core": "^0.
|
|
28
|
+
"kodingo-agent": "^0.2.0",
|
|
29
|
+
"kodingo-core": "^0.3.0",
|
|
30
30
|
"open": "^11.0.0",
|
|
31
31
|
"simple-git": "^3.30.0",
|
|
32
32
|
"tree-sitter-wasms": "^0.1.13",
|