ultra-builder-pro-cli 0.8.1 → 0.10.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/CHANGELOG.md +79 -1
- package/README.md +45 -26
- package/adapters/_shared/codex-assets.cjs +38 -0
- package/adapters/_shared/handbook.cjs +10 -1
- package/adapters/_shared/path-rewrite.cjs +3 -0
- package/adapters/_shared/runtime-assets.cjs +3 -1
- package/adapters/_shared/tests/handbook.test.cjs +9 -1
- package/adapters/_shared/tests/path-rewrite.test.cjs +3 -1
- package/adapters/_shared/tests/runtime-assets.test.cjs +5 -1
- package/adapters/kimi.js +642 -0
- package/adapters/opencode.js +46 -17
- package/adapters/tests/claude.test.cjs +4 -1
- package/adapters/tests/codex.test.cjs +4 -3
- package/adapters/tests/kimi-hook.test.cjs +122 -0
- package/adapters/tests/kimi.test.cjs +259 -0
- package/adapters/tests/opencode.test.cjs +46 -3
- package/bin/handbook.js +2 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +1 -1
- package/docs/AGENT-CONTEXT.md +39 -15
- package/docs/ARCHITECTURE.md +35 -9
- package/docs/PLAN.zh-CN.md +5 -4
- package/docs/ROADMAP.md +8 -7
- package/docs/RUNTIME-COMPAT-MATRIX.md +110 -64
- package/docs/USER-HANDBOOK-CONTRACT.md +13 -3
- package/hooks/README.md +6 -4
- package/hooks/active_task_context.py +14 -0
- package/hooks/adapters/kimi.py +162 -0
- package/hooks/context_spine.py +218 -0
- package/hooks/health_check.py +1 -1
- package/hooks/workflow_context.py +16 -123
- package/hooks/workflow_resume.py +21 -0
- package/mcp-server/lib/change-workflow.cjs +89 -11
- package/mcp-server/lib/change-workflow.test.cjs +202 -7
- package/mcp-server/lib/context-spine.cjs +331 -0
- package/mcp-server/lib/context-spine.test.cjs +73 -0
- package/mcp-server/lib/spec-learning.cjs +191 -0
- package/mcp-server/lib/spec-learning.test.cjs +62 -0
- package/mcp-server/lib/state-db.cjs +149 -4
- package/mcp-server/lib/state-db.test.cjs +61 -0
- package/mcp-server/server.cjs +31 -1
- package/mcp-server/tests/db-ops.test.cjs +3 -3
- package/mcp-server/tests/init-project.test.cjs +2 -2
- package/mcp-server/tests/server-contract.test.cjs +74 -0
- package/mcp-server/tests/state-db.test.cjs +20 -3
- package/package.json +4 -2
- package/skills/learn/SKILL.md +1 -0
- package/skills/ultra-change/SKILL.md +91 -118
- package/skills/ultra-deliver/SKILL.md +93 -221
- package/skills/ultra-dev/SKILL.md +81 -310
- package/skills/ultra-doctor/SKILL.md +1 -0
- package/skills/ultra-init/SKILL.md +1 -0
- package/skills/ultra-plan/SKILL.md +72 -292
- package/skills/ultra-review/SKILL.md +79 -407
- package/skills/ultra-status/SKILL.md +55 -168
- package/skills/ultra-test/SKILL.md +64 -183
- package/skills/ultra-think/SKILL.md +1 -0
- package/spec/cli-protocol.md +3 -0
- package/spec/mcp-tools.yaml +136 -7
- package/spec/schemas/skill-manifest.schema.json +2 -2
- package/spec/schemas/state-db.sql +35 -2
package/hooks/workflow_resume.py
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import sqlite3
|
|
6
7
|
import sys
|
|
7
8
|
import tempfile
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
12
|
+
from context_spine import find_root, read_breadcrumb, render_breadcrumb
|
|
13
|
+
|
|
11
14
|
TERMINAL = {"committed", "completed", "done", "cancelled"}
|
|
12
15
|
|
|
13
16
|
|
|
@@ -86,6 +89,24 @@ def main() -> None:
|
|
|
86
89
|
print(f"[workflow_resume] invalid hook input: {exc}", file=sys.stderr)
|
|
87
90
|
data = {}
|
|
88
91
|
start = Path(data.get("cwd") or Path.cwd()).resolve()
|
|
92
|
+
root = find_root(start)
|
|
93
|
+
if root is not None:
|
|
94
|
+
try:
|
|
95
|
+
breadcrumb = read_breadcrumb(root)
|
|
96
|
+
except (sqlite3.Error, OSError) as exc:
|
|
97
|
+
print(f"[workflow_resume] cannot inspect Context Spine: {exc}", file=sys.stderr)
|
|
98
|
+
breadcrumb = None
|
|
99
|
+
if breadcrumb:
|
|
100
|
+
text = render_breadcrumb(root, breadcrumb)
|
|
101
|
+
event = data.get("hook_event_name", "SessionStart")
|
|
102
|
+
if event == "SessionStart":
|
|
103
|
+
print(json.dumps({"hookSpecificOutput": {
|
|
104
|
+
"hookEventName": "SessionStart",
|
|
105
|
+
"additionalContext": text,
|
|
106
|
+
}}))
|
|
107
|
+
else:
|
|
108
|
+
print(json.dumps({"additionalContext": text}))
|
|
109
|
+
return
|
|
89
110
|
for root in (start, *start.parents):
|
|
90
111
|
state_file = root / ".ultra" / "workflow-state.json"
|
|
91
112
|
checkpoint_file = root / ".ultra" / "runtime" / "checkpoint.json"
|
|
@@ -6,6 +6,8 @@ const path = require('node:path');
|
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
7
|
|
|
8
8
|
const ops = require('./state-ops.cjs');
|
|
9
|
+
const contextSpine = require('./context-spine.cjs');
|
|
10
|
+
const specLearning = require('./spec-learning.cjs');
|
|
9
11
|
|
|
10
12
|
const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
11
13
|
const PROVIDER_KINDS = new Set(['memory', 'code_graph']);
|
|
@@ -227,13 +229,16 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
227
229
|
? change.provider_refs
|
|
228
230
|
: normalizeProviderRefs(input.provider_refs);
|
|
229
231
|
const tasks = db.prepare(
|
|
230
|
-
|
|
232
|
+
`SELECT id, title, type, status, priority, complexity, stale, trace_to,
|
|
233
|
+
context_file, files_modified, session_id, created_at, updated_at
|
|
234
|
+
FROM tasks WHERE change_id = ? ORDER BY created_at ASC`,
|
|
231
235
|
).all(change.id).map((task) => ({ ...task, stale: Boolean(task.stale) }));
|
|
232
236
|
const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
|
|
233
237
|
const allowedPaths = Array.isArray(input.allowed_paths) ? input.allowed_paths : [];
|
|
234
238
|
const head = gitHead(rootDir);
|
|
239
|
+
const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
|
|
235
240
|
const manifest = {
|
|
236
|
-
schema_version: '
|
|
241
|
+
schema_version: '2.0',
|
|
237
242
|
generated_at: nowIso(),
|
|
238
243
|
change: {
|
|
239
244
|
id: change.id, title: change.title, kind: change.kind, status: change.status,
|
|
@@ -241,7 +246,15 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
241
246
|
},
|
|
242
247
|
git: { head },
|
|
243
248
|
tasks,
|
|
249
|
+
selected_task: spine.selected_task,
|
|
244
250
|
specs,
|
|
251
|
+
role: spine.role,
|
|
252
|
+
gate: spine.gate,
|
|
253
|
+
next_action: spine.next_action,
|
|
254
|
+
readiness: spine.readiness,
|
|
255
|
+
context: spine.context,
|
|
256
|
+
execution_contract: spine.execution_contract,
|
|
257
|
+
resume: spine.resume,
|
|
245
258
|
providers,
|
|
246
259
|
provider_boundary: 'metadata references only; memory and code graph content remain external',
|
|
247
260
|
tool_policy: { allowed_paths: allowedPaths },
|
|
@@ -257,15 +270,24 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
257
270
|
ops.tx(db, () => {
|
|
258
271
|
db.prepare(
|
|
259
272
|
`INSERT INTO context_snapshots
|
|
260
|
-
(id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash
|
|
261
|
-
|
|
273
|
+
(id, change_id, task_id, git_head, provider_refs_json, manifest_path, manifest_hash,
|
|
274
|
+
role, gate, next_action, readiness, blockers_json, context_json,
|
|
275
|
+
token_estimate, token_budget)
|
|
276
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
262
277
|
).run(
|
|
263
278
|
`ctx-${crypto.randomUUID().slice(0, 12)}`, change.id, input.task_id || null,
|
|
264
279
|
head, JSON.stringify(providers), relative, hash,
|
|
280
|
+
spine.role, spine.gate, spine.next_action, spine.readiness.status,
|
|
281
|
+
JSON.stringify(spine.readiness.blockers), JSON.stringify(spine),
|
|
282
|
+
spine.context.token_estimate, spine.context.budget.max_tokens,
|
|
265
283
|
);
|
|
266
284
|
upsertArtifact(db, {
|
|
267
285
|
change_id: change.id, task_id: input.task_id || null, kind: 'context_manifest',
|
|
268
|
-
artifactPath: relative, contentHash: hash,
|
|
286
|
+
artifactPath: relative, contentHash: hash,
|
|
287
|
+
metadata: {
|
|
288
|
+
git_head: head, role: spine.role, gate: spine.gate,
|
|
289
|
+
readiness: spine.readiness.status, token_estimate: spine.context.token_estimate,
|
|
290
|
+
},
|
|
269
291
|
});
|
|
270
292
|
for (const task of tasks) {
|
|
271
293
|
if (!task.trace_to) continue;
|
|
@@ -278,7 +300,11 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
278
300
|
.run(JSON.stringify(providers), nowIso(), change.id);
|
|
279
301
|
ops.appendEventInTx(db, {
|
|
280
302
|
type: 'context_updated', change_id: change.id, task_id: input.task_id || null,
|
|
281
|
-
payload: {
|
|
303
|
+
payload: {
|
|
304
|
+
manifest_path: relative, manifest_hash: hash, git_head: head,
|
|
305
|
+
role: spine.role, gate: spine.gate, readiness: spine.readiness.status,
|
|
306
|
+
next_action: spine.next_action,
|
|
307
|
+
},
|
|
282
308
|
});
|
|
283
309
|
});
|
|
284
310
|
return { manifest, context_manifest_path: manifestPath, manifest_hash: hash };
|
|
@@ -438,16 +464,43 @@ function filesUnder(dir) {
|
|
|
438
464
|
return out;
|
|
439
465
|
}
|
|
440
466
|
|
|
441
|
-
function evidenceBlockers(change, evidence) {
|
|
467
|
+
function evidenceBlockers(change, evidence, tasks) {
|
|
442
468
|
const blockers = [];
|
|
443
469
|
const rows = Array.isArray(evidence) ? evidence : [];
|
|
444
470
|
const byCategory = new Map(rows.map((row) => [row.category, row]));
|
|
445
|
-
for (const category of REQUIRED_EVIDENCE[change.kind]) {
|
|
471
|
+
for (const category of REQUIRED_EVIDENCE[change.kind].filter((value) => value !== 'review')) {
|
|
446
472
|
const row = byCategory.get(category);
|
|
447
473
|
if (!row) blockers.push(`EVIDENCE_MISSING:${category}`);
|
|
448
474
|
else if (!['pass', 'not_applicable'].includes(row.status)) blockers.push(`EVIDENCE_FAILED:${category}`);
|
|
449
475
|
else if (!row.evidence || String(row.evidence).trim().length < 3) blockers.push(`EVIDENCE_EMPTY:${category}`);
|
|
450
476
|
}
|
|
477
|
+
if (['standard', 'major'].includes(change.kind)) {
|
|
478
|
+
const reviews = rows.filter((row) => row.category === 'review');
|
|
479
|
+
for (const axis of ['spec_fidelity', 'engineering_standards']) {
|
|
480
|
+
const row = reviews.find((candidate) => candidate.axis === axis);
|
|
481
|
+
if (!row) blockers.push(`REVIEW_AXIS_MISSING:${axis}`);
|
|
482
|
+
else if (row.status !== 'pass') blockers.push(`REVIEW_AXIS_FAILED:${axis}`);
|
|
483
|
+
else if (!row.evidence || String(row.evidence).trim().length < 3) {
|
|
484
|
+
blockers.push(`REVIEW_AXIS_EMPTY:${axis}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const tests = byCategory.get('tests');
|
|
489
|
+
if (tests && ['standard', 'major', 'incident'].includes(change.kind)
|
|
490
|
+
&& (!tests.seam || String(tests.seam).trim().length < 3)) {
|
|
491
|
+
blockers.push('TEST_SEAM_MISSING');
|
|
492
|
+
}
|
|
493
|
+
if (tests && (change.kind === 'incident' || tasks.some((task) => task.type === 'bugfix'))) {
|
|
494
|
+
const signal = tests.signal;
|
|
495
|
+
if (!signal) blockers.push('TEST_RED_SIGNAL_MISSING');
|
|
496
|
+
else {
|
|
497
|
+
if (!signal.command || String(signal.command).trim().length < 3) blockers.push('TEST_SIGNAL_COMMAND_MISSING');
|
|
498
|
+
if (!signal.expected_red || String(signal.expected_red).trim().length < 3) blockers.push('TEST_EXPECTED_RED_MISSING');
|
|
499
|
+
if (signal.observed_red !== true) blockers.push('TEST_RED_NOT_OBSERVED');
|
|
500
|
+
if (signal.observed_green !== true) blockers.push('TEST_GREEN_NOT_OBSERVED');
|
|
501
|
+
if (signal.deterministic !== true) blockers.push('TEST_SIGNAL_NOT_DETERMINISTIC');
|
|
502
|
+
}
|
|
503
|
+
}
|
|
451
504
|
return blockers;
|
|
452
505
|
}
|
|
453
506
|
|
|
@@ -457,11 +510,31 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
457
510
|
if (!['active', 'blocked'].includes(change.status)) {
|
|
458
511
|
throw new ChangeWorkflowError('CHANGE_NOT_CONVERGEABLE', `change ${input.id} is ${change.status}`);
|
|
459
512
|
}
|
|
460
|
-
const
|
|
461
|
-
const
|
|
513
|
+
const tasks = db.prepare('SELECT id, type, status, stale FROM tasks WHERE change_id = ? ORDER BY id').all(change.id);
|
|
514
|
+
const blockers = new Set(evidenceBlockers(change, input.evidence, tasks));
|
|
462
515
|
if (tasks.length === 0) blockers.add('NO_TASKS');
|
|
463
516
|
if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
|
|
464
517
|
if (tasks.some((task) => Boolean(task.stale))) blockers.add('TASK_CONTEXT_STALE');
|
|
518
|
+
for (const task of tasks) {
|
|
519
|
+
const snapshot = db.prepare(
|
|
520
|
+
`SELECT readiness, context_json FROM context_snapshots
|
|
521
|
+
WHERE change_id = ? AND task_id = ?
|
|
522
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
523
|
+
).get(change.id, task.id);
|
|
524
|
+
if (!snapshot) {
|
|
525
|
+
blockers.add(`TASK_CONTEXT_MISSING:${task.id}`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const context = parseJson(snapshot.context_json, 'context_snapshots.context_json');
|
|
529
|
+
if (snapshot.readiness !== 'ready') blockers.add(`TASK_CONTEXT_NOT_READY:${task.id}`);
|
|
530
|
+
if (!context.execution_contract?.public_seam) blockers.add(`TASK_SEAM_MISSING:${task.id}`);
|
|
531
|
+
if (!context.execution_contract?.verification_command) blockers.add(`TASK_VERIFICATION_MISSING:${task.id}`);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (const candidate of specLearning.listSpecLearning(db, change.id)) {
|
|
535
|
+
if (candidate.status === 'proposed') blockers.add(`SPEC_LEARNING_UNRESOLVED:${candidate.id}`);
|
|
536
|
+
if (candidate.status === 'approved') blockers.add(`SPEC_LEARNING_NOT_APPLIED:${candidate.id}`);
|
|
537
|
+
}
|
|
465
538
|
|
|
466
539
|
if (change.docs_impact.status === 'unknown') blockers.add('DOCS_IMPACT_UNKNOWN');
|
|
467
540
|
if (change.docs_impact.status === 'none' && (!change.docs_impact.rationale || change.docs_impact.rationale.length < 3)) {
|
|
@@ -513,7 +586,8 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
|
|
|
513
586
|
const lines = [
|
|
514
587
|
`# Verification: ${change.title}`, '',
|
|
515
588
|
...input.evidence.flatMap((row) => [
|
|
516
|
-
`## ${row.category}
|
|
589
|
+
`## ${row.category}${row.axis ? `:${row.axis}` : ''}`, '',
|
|
590
|
+
`Status: **${row.status}**`, '', String(row.evidence).trim(), '',
|
|
517
591
|
]),
|
|
518
592
|
];
|
|
519
593
|
fs.writeFileSync(verificationPath, `${lines.join('\n')}\n`);
|
|
@@ -618,6 +692,10 @@ module.exports = {
|
|
|
618
692
|
listChanges,
|
|
619
693
|
updateChange,
|
|
620
694
|
compileContext,
|
|
695
|
+
readBreadcrumb: contextSpine.readBreadcrumb,
|
|
696
|
+
proposeSpecLearning: specLearning.proposeSpecLearning,
|
|
697
|
+
resolveSpecLearning: specLearning.resolveSpecLearning,
|
|
698
|
+
listSpecLearning: specLearning.listSpecLearning,
|
|
621
699
|
convergeChange,
|
|
622
700
|
archiveChange,
|
|
623
701
|
normalizeDocsImpact,
|
|
@@ -31,10 +31,20 @@ function cleanup({ rootDir, db }) {
|
|
|
31
31
|
function standardEvidence() {
|
|
32
32
|
return [
|
|
33
33
|
{ category: 'diff', status: 'pass', evidence: 'git diff reviewed' },
|
|
34
|
-
{
|
|
34
|
+
{
|
|
35
|
+
category: 'tests', status: 'pass', evidence: 'node --test passed',
|
|
36
|
+
seam: 'public change workflow contract',
|
|
37
|
+
},
|
|
35
38
|
{ category: 'spec', status: 'pass', evidence: 'delta reconciled' },
|
|
36
39
|
{ category: 'docs', status: 'pass', evidence: 'docs/feature.md updated' },
|
|
37
|
-
{
|
|
40
|
+
{
|
|
41
|
+
category: 'review', axis: 'spec_fidelity', status: 'pass',
|
|
42
|
+
evidence: 'Acceptance behavior matches the approved delta.',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
category: 'review', axis: 'engineering_standards', status: 'pass',
|
|
46
|
+
evidence: 'Engineering findings are resolved.',
|
|
47
|
+
},
|
|
38
48
|
];
|
|
39
49
|
}
|
|
40
50
|
|
|
@@ -42,10 +52,43 @@ function incidentEvidence() {
|
|
|
42
52
|
return [
|
|
43
53
|
{ category: 'diagnosis', status: 'pass', evidence: 'Structured diagnosis artifact reviewed.' },
|
|
44
54
|
{ category: 'diff', status: 'pass', evidence: 'Root-cause fix diff reviewed.' },
|
|
45
|
-
{
|
|
55
|
+
{
|
|
56
|
+
category: 'tests', status: 'pass', evidence: 'Regression test reproduces then verifies the fix.',
|
|
57
|
+
seam: 'projection recovery boundary',
|
|
58
|
+
signal: {
|
|
59
|
+
command: 'node --test recovery.test.cjs',
|
|
60
|
+
expected_red: 'stale projection remains running',
|
|
61
|
+
observed_red: true,
|
|
62
|
+
observed_green: true,
|
|
63
|
+
deterministic: true,
|
|
64
|
+
duration_ms: 25,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
46
67
|
];
|
|
47
68
|
}
|
|
48
69
|
|
|
70
|
+
function executionContext(taskId, overrides = {}) {
|
|
71
|
+
return {
|
|
72
|
+
task_id: taskId,
|
|
73
|
+
role: 'implement',
|
|
74
|
+
gate: 'implementation',
|
|
75
|
+
next_action: `Implement and verify ${taskId}.`,
|
|
76
|
+
context_refs: [
|
|
77
|
+
{
|
|
78
|
+
ref: 'README.md', kind: 'spec', reason: 'Public behavior contract', required: true,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
budget: { max_tokens: 2_000, max_files: 4 },
|
|
82
|
+
execution_contract: {
|
|
83
|
+
slice_kind: 'tracer_bullet',
|
|
84
|
+
public_seam: `public seam for ${taskId}`,
|
|
85
|
+
verification_command: `node --test ${taskId}.test.cjs`,
|
|
86
|
+
context_budget_percent: 40,
|
|
87
|
+
},
|
|
88
|
+
...overrides,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
49
92
|
test('createChange persists a change and an inspectable external-provider context manifest', () => {
|
|
50
93
|
const fx = fixture();
|
|
51
94
|
try {
|
|
@@ -120,7 +163,9 @@ test('convergeChange blocks incomplete work and marks a fully evidenced standard
|
|
|
120
163
|
});
|
|
121
164
|
ops.updateTaskStatus(fx.db, 'change-task', 'in_progress');
|
|
122
165
|
ops.updateTaskStatus(fx.db, 'change-task', 'completed');
|
|
123
|
-
changes.compileContext(fx.db, {
|
|
166
|
+
changes.compileContext(fx.db, {
|
|
167
|
+
id: 'chg-converge', ...executionContext('change-task'),
|
|
168
|
+
}, { rootDir: fx.rootDir });
|
|
124
169
|
|
|
125
170
|
const ready = changes.convergeChange(fx.db, {
|
|
126
171
|
id: 'chg-converge', evidence: standardEvidence(),
|
|
@@ -148,12 +193,20 @@ test('archiveChange moves a ready change into immutable history and records base
|
|
|
148
193
|
});
|
|
149
194
|
ops.updateTaskStatus(fx.db, 'archive-task', 'in_progress');
|
|
150
195
|
ops.updateTaskStatus(fx.db, 'archive-task', 'completed');
|
|
151
|
-
changes.compileContext(fx.db, {
|
|
196
|
+
changes.compileContext(fx.db, {
|
|
197
|
+
id: 'chg-archive', ...executionContext('archive-task'),
|
|
198
|
+
}, { rootDir: fx.rootDir });
|
|
152
199
|
const converged = changes.convergeChange(fx.db, {
|
|
153
200
|
id: 'chg-archive',
|
|
154
201
|
evidence: [
|
|
155
202
|
{ category: 'diff', status: 'pass', evidence: 'diff reviewed' },
|
|
156
|
-
{
|
|
203
|
+
{
|
|
204
|
+
category: 'tests', status: 'pass', evidence: 'tests passed', seam: 'archive command',
|
|
205
|
+
signal: {
|
|
206
|
+
command: 'node --test archive-task.test.cjs', expected_red: 'archive accepts stale context',
|
|
207
|
+
observed_red: true, observed_green: true, deterministic: true, duration_ms: 10,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
157
210
|
{ category: 'spec', status: 'not_applicable', evidence: 'No behavior contract changed.' },
|
|
158
211
|
],
|
|
159
212
|
}, { rootDir: fx.rootDir });
|
|
@@ -306,7 +359,9 @@ test('incident convergence requires every structured diagnosis section and refre
|
|
|
306
359
|
});
|
|
307
360
|
ops.updateTaskStatus(fx.db, 'incident-task', 'in_progress');
|
|
308
361
|
ops.updateTaskStatus(fx.db, 'incident-task', 'completed');
|
|
309
|
-
changes.compileContext(fx.db, {
|
|
362
|
+
changes.compileContext(fx.db, {
|
|
363
|
+
id: 'chg-incident-converge', ...executionContext('incident-task'),
|
|
364
|
+
}, { rootDir: fx.rootDir });
|
|
310
365
|
|
|
311
366
|
const blocked = changes.convergeChange(fx.db, {
|
|
312
367
|
id: 'chg-incident-converge', evidence: incidentEvidence(),
|
|
@@ -344,3 +399,143 @@ test('incident convergence requires every structured diagnosis section and refre
|
|
|
344
399
|
cleanup(fx);
|
|
345
400
|
}
|
|
346
401
|
});
|
|
402
|
+
|
|
403
|
+
test('compileContext v2 persists role-scoped context, budget, execution contract, and breadcrumb in state.db', () => {
|
|
404
|
+
const fx = fixture();
|
|
405
|
+
try {
|
|
406
|
+
changes.createChange(fx.db, {
|
|
407
|
+
id: 'chg-context-v2', title: 'Compile role context', kind: 'standard',
|
|
408
|
+
intent: 'Give each execution role only the context it needs.',
|
|
409
|
+
}, { rootDir: fx.rootDir });
|
|
410
|
+
ops.createTask(fx.db, {
|
|
411
|
+
id: 'context-v2-task', title: 'Build role-scoped manifest', type: 'feature', priority: 'P0',
|
|
412
|
+
change_id: 'chg-context-v2',
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const compiled = changes.compileContext(fx.db, {
|
|
416
|
+
id: 'chg-context-v2',
|
|
417
|
+
...executionContext('context-v2-task'),
|
|
418
|
+
}, { rootDir: fx.rootDir });
|
|
419
|
+
|
|
420
|
+
assert.equal(compiled.manifest.schema_version, '2.0');
|
|
421
|
+
assert.equal(compiled.manifest.role, 'implement');
|
|
422
|
+
assert.equal(compiled.manifest.gate, 'implementation');
|
|
423
|
+
assert.equal(compiled.manifest.readiness.status, 'ready');
|
|
424
|
+
assert.equal(compiled.manifest.context.items[0].ref, 'README.md');
|
|
425
|
+
assert.match(compiled.manifest.context.items[0].digest, /^[0-9a-f]{64}$/);
|
|
426
|
+
assert.equal(compiled.manifest.execution_contract.slice_kind, 'tracer_bullet');
|
|
427
|
+
assert.equal(compiled.manifest.resume.task_id, 'context-v2-task');
|
|
428
|
+
assert.match(compiled.manifest.next_action, /Implement and verify/);
|
|
429
|
+
|
|
430
|
+
const snapshot = fx.db.prepare(
|
|
431
|
+
'SELECT role, gate, next_action, readiness, context_json, token_budget FROM context_snapshots ORDER BY created_at DESC LIMIT 1',
|
|
432
|
+
).get();
|
|
433
|
+
assert.equal(snapshot.role, 'implement');
|
|
434
|
+
assert.equal(snapshot.gate, 'implementation');
|
|
435
|
+
assert.equal(snapshot.readiness, 'ready');
|
|
436
|
+
assert.equal(snapshot.token_budget, 2_000);
|
|
437
|
+
assert.equal(JSON.parse(snapshot.context_json).execution_contract.public_seam, 'public seam for context-v2-task');
|
|
438
|
+
|
|
439
|
+
const breadcrumb = changes.readBreadcrumb(fx.db, { id: 'chg-context-v2' }, { rootDir: fx.rootDir });
|
|
440
|
+
assert.equal(breadcrumb.change_id, 'chg-context-v2');
|
|
441
|
+
assert.equal(breadcrumb.task_id, 'context-v2-task');
|
|
442
|
+
assert.equal(breadcrumb.role, 'implement');
|
|
443
|
+
assert.equal(breadcrumb.readiness, 'ready');
|
|
444
|
+
assert.match(breadcrumb.context_manifest_hash, /^[0-9a-f]{64}$/);
|
|
445
|
+
} finally {
|
|
446
|
+
cleanup(fx);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('compileContext blocks a required missing ref or an over-budget role packet', () => {
|
|
451
|
+
const fx = fixture();
|
|
452
|
+
try {
|
|
453
|
+
changes.createChange(fx.db, {
|
|
454
|
+
id: 'chg-context-budget', title: 'Bound context packet', kind: 'quick',
|
|
455
|
+
intent: 'Fail closed when required context is missing or too large.',
|
|
456
|
+
}, { rootDir: fx.rootDir });
|
|
457
|
+
ops.createTask(fx.db, {
|
|
458
|
+
id: 'budget-task', title: 'Compile bounded packet', type: 'bugfix', priority: 'P0',
|
|
459
|
+
change_id: 'chg-context-budget',
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const compiled = changes.compileContext(fx.db, {
|
|
463
|
+
id: 'chg-context-budget',
|
|
464
|
+
...executionContext('budget-task', {
|
|
465
|
+
context_refs: [
|
|
466
|
+
{ ref: 'missing-contract.md', kind: 'spec', reason: 'Required contract', required: true },
|
|
467
|
+
{ ref: 'README.md', kind: 'source', reason: 'Current implementation', required: true },
|
|
468
|
+
],
|
|
469
|
+
budget: { max_tokens: 1, max_files: 1 },
|
|
470
|
+
}),
|
|
471
|
+
}, { rootDir: fx.rootDir });
|
|
472
|
+
|
|
473
|
+
assert.equal(compiled.manifest.readiness.status, 'blocked');
|
|
474
|
+
assert.ok(compiled.manifest.readiness.blockers.includes('CONTEXT_REQUIRED_REF_MISSING:missing-contract.md'));
|
|
475
|
+
assert.ok(compiled.manifest.readiness.blockers.includes('CONTEXT_FILE_BUDGET_EXCEEDED'));
|
|
476
|
+
assert.ok(compiled.manifest.readiness.blockers.includes('CONTEXT_TOKEN_BUDGET_EXCEEDED'));
|
|
477
|
+
} finally {
|
|
478
|
+
cleanup(fx);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('spec-learning candidates are approval-gated and unresolved candidates block convergence', () => {
|
|
483
|
+
const fx = fixture();
|
|
484
|
+
try {
|
|
485
|
+
changes.createChange(fx.db, {
|
|
486
|
+
id: 'chg-learning', title: 'Converge learned contract', kind: 'quick',
|
|
487
|
+
intent: 'Promote stable implementation discoveries into an approved baseline update.',
|
|
488
|
+
docs_impact: { status: 'none', files: [], rationale: 'Fixture exercises the approval state machine.' },
|
|
489
|
+
}, { rootDir: fx.rootDir });
|
|
490
|
+
ops.createTask(fx.db, {
|
|
491
|
+
id: 'learning-task', title: 'Capture stable contract', type: 'bugfix', priority: 'P0',
|
|
492
|
+
change_id: 'chg-learning',
|
|
493
|
+
});
|
|
494
|
+
ops.updateTaskStatus(fx.db, 'learning-task', 'in_progress');
|
|
495
|
+
ops.updateTaskStatus(fx.db, 'learning-task', 'completed');
|
|
496
|
+
changes.compileContext(fx.db, {
|
|
497
|
+
id: 'chg-learning', ...executionContext('learning-task'),
|
|
498
|
+
}, { rootDir: fx.rootDir });
|
|
499
|
+
|
|
500
|
+
const proposed = changes.proposeSpecLearning(fx.db, {
|
|
501
|
+
id: 'learning-1', change_id: 'chg-learning', task_id: 'learning-task',
|
|
502
|
+
target_ref: 'README.md#contract', summary: 'The public command fails closed on stale context.',
|
|
503
|
+
evidence: ['node --test learning-task.test.cjs'],
|
|
504
|
+
}, { rootDir: fx.rootDir });
|
|
505
|
+
assert.equal(proposed.status, 'proposed');
|
|
506
|
+
|
|
507
|
+
const blocked = changes.convergeChange(fx.db, {
|
|
508
|
+
id: 'chg-learning',
|
|
509
|
+
evidence: [
|
|
510
|
+
{ category: 'diff', status: 'pass', evidence: 'diff reviewed' },
|
|
511
|
+
{
|
|
512
|
+
category: 'tests', status: 'pass', evidence: 'regression passed', seam: 'public command',
|
|
513
|
+
signal: {
|
|
514
|
+
command: 'node --test learning-task.test.cjs', expected_red: 'stale context accepted',
|
|
515
|
+
observed_red: true, observed_green: true, deterministic: true, duration_ms: 20,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{ category: 'spec', status: 'pass', evidence: 'candidate recorded' },
|
|
519
|
+
],
|
|
520
|
+
}, { rootDir: fx.rootDir });
|
|
521
|
+
assert.ok(blocked.blockers.includes('SPEC_LEARNING_UNRESOLVED:learning-1'));
|
|
522
|
+
|
|
523
|
+
assert.equal(changes.resolveSpecLearning(fx.db, {
|
|
524
|
+
change_id: 'chg-learning', candidate_id: 'learning-1', decision: 'approve',
|
|
525
|
+
resolution: 'Stable public contract.',
|
|
526
|
+
}, { rootDir: fx.rootDir }).status, 'approved');
|
|
527
|
+
assert.equal(changes.resolveSpecLearning(fx.db, {
|
|
528
|
+
change_id: 'chg-learning', candidate_id: 'learning-1', decision: 'apply',
|
|
529
|
+
resolution: 'Applied to README.md#contract.',
|
|
530
|
+
}, { rootDir: fx.rootDir }).status, 'applied');
|
|
531
|
+
|
|
532
|
+
const projection = JSON.parse(fs.readFileSync(
|
|
533
|
+
path.join(fx.rootDir, '.ultra', 'changes', 'active', 'chg-learning', 'spec-learning.json'),
|
|
534
|
+
'utf8',
|
|
535
|
+
));
|
|
536
|
+
assert.equal(projection.source, '.ultra/state.db');
|
|
537
|
+
assert.equal(projection.candidates[0].status, 'applied');
|
|
538
|
+
} finally {
|
|
539
|
+
cleanup(fx);
|
|
540
|
+
}
|
|
541
|
+
});
|