@haaaiawd/loom 2.1.0 → 2.1.2
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 +46 -35
- package/EVIL_EVAL.md +22 -22
- package/README.md +26 -15
- package/README.zh-CN.md +24 -21
- package/cli/bin/loom.js +94 -26
- package/cli/src/protocol.js +54 -21
- package/cli/src/store.js +108 -44
- package/design.md +58 -44
- package/docs/PROMPT_CATALOG.md +31 -31
- package/docs/RELEASE_CHECKLIST.md +3 -3
- package/docs/UX_FLOW.md +18 -18
- package/package.json +9 -5
package/cli/src/protocol.js
CHANGED
|
@@ -75,10 +75,12 @@ list. Each decision node carries an entry condition, options, judgment criteria,
|
|
|
75
75
|
a counterexample, and an output. Nodes without sources are not accepted; branches without
|
|
76
76
|
counterexamples are fixed steps in disguise.
|
|
77
77
|
|
|
78
|
-
Build dossiers in four steps: research (collect expert narratives, case studies, and methodology
|
|
79
|
-
sources), synthesize (construct the project-specific decision tree from research),
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
Build dossiers in four steps: research (collect expert narratives, case studies, and methodology
|
|
79
|
+
sources), synthesize (construct the project-specific decision tree from research), select the scenario,
|
|
80
|
+
and confirm its authority. Use \`loom capability confirm <slug> --scenario <text> --source human\` when
|
|
81
|
+
the user confirms which expert situation applies. Use \`--source agent\` only for a reversible provisional
|
|
82
|
+
selection when the human is unavailable; it remains visibly provisional until a human confirms it.
|
|
83
|
+
\`loom capability research\` creates a \`_guide.md\` in the research directory explaining
|
|
82
84
|
what to write — create one .md file per source, citing where the knowledge came from. \`loom capability
|
|
83
85
|
synthesize\` reads those files and validates that every decision tree node has a source citation and a
|
|
84
86
|
counterexample. A dossier that has not been confirmed should not be referenced by Tasks, but the
|
|
@@ -110,8 +112,12 @@ Each Task records:
|
|
|
110
112
|
- **reads**: every file, document, or artifact the Agent must consume to do this Task. Must be specific
|
|
111
113
|
paths, not categories. If the Agent needs it, list it; if it is not listed, the Agent should not
|
|
112
114
|
depend on it.
|
|
113
|
-
- **touches**: every file, document, or artifact the Task is expected to produce or modify. Must be
|
|
114
|
-
specific paths. A Task that touches nothing is not a Task.
|
|
115
|
+
- **touches**: every file, document, or artifact the Task is expected to produce or modify. Must be
|
|
116
|
+
specific paths. A Task that touches nothing is not a Task.
|
|
117
|
+
- **implements**: the design decision this Task realizes. If no design applies, use a concrete
|
|
118
|
+
\`design_exemption\` instead of leaving the relationship implicit.
|
|
119
|
+
- **capability_hooks**: the professional decision-tree nodes activated by this Task. If no dossier
|
|
120
|
+
applies, use a concrete \`capability_exemption\`; an empty array alone is not a decision.
|
|
115
121
|
- **depends_on**: other Tasks that must be done first. Empty is valid only when this Task has no
|
|
116
122
|
prerequisites.
|
|
117
123
|
- **covers**: which delivery units this Task advances. Use this to check that the delivery surface is
|
|
@@ -216,17 +222,34 @@ The human's patience is the project's fuel. Do not let several Tasks pass in sil
|
|
|
216
222
|
output, a visualization, a working demo — reach an early slice of it sooner rather than saving it for
|
|
217
223
|
last. The human's "I want to see more" feeling is real project fuel.
|
|
218
224
|
|
|
225
|
+
## Handle user feedback before changing code
|
|
226
|
+
|
|
227
|
+
When the human gives feedback, a fix suggestion, or a direction change, do not jump straight to editing
|
|
228
|
+
code. First judge whether the feedback changes the project's design intent, Task scope, or acceptance
|
|
229
|
+
conditions.
|
|
230
|
+
|
|
231
|
+
- **Consequential feedback** (changes design, scope, boundaries, or what "done" means): record it with
|
|
232
|
+
\`loom decision --json-file\` before editing. Update the affected design document or Task. If a done Task
|
|
233
|
+
is affected, \`loom check\` will warn — reopen it if the change invalidates prior evidence. Then edit
|
|
234
|
+
code.
|
|
235
|
+
- **Implementation detail** (a small bug, a color tweak, a wording fix, a test that broke): fix it
|
|
236
|
+
directly. No decision record needed.
|
|
237
|
+
- **When you are not sure**: lean toward recording. A decision record for a small change costs almost
|
|
238
|
+
nothing; skipping one for a consequential change creates silent drift that is expensive to trace later.
|
|
239
|
+
|
|
219
240
|
Do not narrate this protocol to the human unless it helps them understand a decision, risk, or handoff.`;
|
|
220
241
|
|
|
221
242
|
export const AGENT_ANCHOR = `<!-- loom:v2 -->
|
|
222
243
|
## LOOM
|
|
223
244
|
|
|
224
|
-
This project uses LOOM as Agent-only continuity infrastructure. Run \`loom context\` when entering the
|
|
225
|
-
project, after a context reset,
|
|
226
|
-
its execution protocol, exact state, and declared
|
|
227
|
-
Keep project truth, design documents, professional
|
|
228
|
-
and never ask the human to operate LOOM. Do not
|
|
229
|
-
meaningful work boundaries
|
|
245
|
+
This project uses LOOM as Agent-only continuity infrastructure. Run \`loom context\` when entering the
|
|
246
|
+
project, after a context reset, before substantial work, and when the human gives feedback or a direction
|
|
247
|
+
change. When a Task is active, the command restores its execution protocol, exact state, and declared
|
|
248
|
+
context; rerun it before editing after any interruption. Keep project truth, design documents, professional
|
|
249
|
+
capability dossiers, and Tasks current through the CLI, and never ask the human to operate LOOM. Do not
|
|
250
|
+
rerun LOOM before every tool call: persist and restore at meaningful work boundaries. The installed
|
|
251
|
+
\`loom\` command is the stable project-local invocation. If it is not on PATH in a source checkout, use
|
|
252
|
+
the absolute path to that checkout's \`cli/bin/loom.js\`; do not guess a relative path from this project.`;
|
|
230
253
|
|
|
231
254
|
function renderKeeperGap(item) {
|
|
232
255
|
if (typeof item === 'string') return ` - ${item}`;
|
|
@@ -341,9 +364,14 @@ after the gaps are fixed without requiring another Keeper round:
|
|
|
341
364
|
\`\`\`json
|
|
342
365
|
{
|
|
343
366
|
"run_id": "<unique-id>",
|
|
344
|
-
"prepared_digest": "<digest-above>",
|
|
345
|
-
"verdict": "passed | needs_revision | blocked",
|
|
346
|
-
"
|
|
367
|
+
"prepared_digest": "<digest-above>",
|
|
368
|
+
"verdict": "passed | needs_revision | blocked",
|
|
369
|
+
"review": {
|
|
370
|
+
"mode": "independent | self",
|
|
371
|
+
"reviewer_id": "<fresh Agent or current Agent identity>",
|
|
372
|
+
"evidence": "<how the host isolated this review from shaping context>"
|
|
373
|
+
},
|
|
374
|
+
"summary": "<concise handoff judgment>",
|
|
347
375
|
"gaps": [
|
|
348
376
|
{
|
|
349
377
|
"gap": "<missing or contradictory truth>",
|
|
@@ -356,8 +384,12 @@ after the gaps are fixed without requiring another Keeper round:
|
|
|
356
384
|
}
|
|
357
385
|
\`\`\`
|
|
358
386
|
|
|
359
|
-
Record the result with \`loom keeper record --json-file <result.json>\`. Use \`passed\` only when you
|
|
360
|
-
could responsibly begin the first Task
|
|
387
|
+
Record the result with \`loom keeper record --json-file <result.json>\`. Use \`passed\` only when you are
|
|
388
|
+
a genuinely fresh Agent, could responsibly begin the first Task, and can record \`review.mode\` as
|
|
389
|
+
\`independent\` with concrete isolation evidence.
|
|
390
|
+
If you shaped this project, do not review your own work as independent; ask the host for a fresh Agent,
|
|
391
|
+
or use \`loom keeper skip --reason <concrete limitation>\` when isolation is unavailable. Otherwise use
|
|
392
|
+
\`needs_revision\` with concrete gaps and the
|
|
361
393
|
observable evidence that would close each one, or \`blocked\` when progress requires unavailable
|
|
362
394
|
authority or external state. A failed attempt returns the project to shaping; revision requires a
|
|
363
395
|
changed digest. When all gaps are minor and 3 or fewer, fixing them and running \`loom project ready\`
|
|
@@ -395,7 +427,8 @@ decision surface it owns. Complex subsystems should have their own files under \
|
|
|
395
427
|
|
|
396
428
|
Link each separate field dossier under \`.loom/capabilities/<field>/capability.md\` and state which
|
|
397
429
|
design decisions it changes. Do not merge distinct fields into one dossier. Capabilities are shaped in
|
|
398
|
-
four steps: \`loom capability research\` → \`synthesize\` → \`confirm\` (user
|
|
430
|
+
four steps: \`loom capability research\` → \`synthesize\` → \`confirm --source human\` (user-confirmed)
|
|
431
|
+
or \`confirm --source agent\` (explicitly provisional when the human is unavailable).
|
|
399
432
|
|
|
400
433
|
## Project structure
|
|
401
434
|
|
|
@@ -521,9 +554,9 @@ Name the established field, what expertise it contributes, and what belongs to a
|
|
|
521
554
|
|
|
522
555
|
## Project scenario
|
|
523
556
|
|
|
524
|
-
> This section records
|
|
525
|
-
>
|
|
526
|
-
>
|
|
557
|
+
> This section records which expert situation the project most closely resembles. Use
|
|
558
|
+
> \`loom capability confirm <slug> --scenario <text> --source human\` for user confirmation. When the
|
|
559
|
+
> human is unavailable, use \`--source agent\`; LOOM records the selection as provisional.
|
|
527
560
|
|
|
528
561
|
State the project scenario that determines which branches of the decision tree are active.
|
|
529
562
|
|
package/cli/src/store.js
CHANGED
|
@@ -196,7 +196,10 @@ function validateTasks(tasks) {
|
|
|
196
196
|
if (acc.evidence !== undefined && typeof acc.evidence !== 'string') throw new Error(`${task.id} acceptance evidence must be a string`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
|
-
if (task.implements !== undefined && typeof task.implements !== 'string') throw new Error(`${task.id} implements must be a string referencing a design decision`);
|
|
199
|
+
if (task.implements !== undefined && typeof task.implements !== 'string') throw new Error(`${task.id} implements must be a string referencing a design decision`);
|
|
200
|
+
if (task.design_exemption !== undefined && (typeof task.design_exemption !== 'string' || (task.design_exemption && task.design_exemption.length < 10))) throw new Error(`${task.id} design_exemption must be a concrete reason`);
|
|
201
|
+
if (task.capability_exemption !== undefined && (typeof task.capability_exemption !== 'string' || (task.capability_exemption && task.capability_exemption.length < 10))) throw new Error(`${task.id} capability_exemption must be a concrete reason`);
|
|
202
|
+
if (task.integrity_version !== undefined && task.integrity_version !== 1) throw new Error(`${task.id} integrity_version is unsupported`);
|
|
200
203
|
if (task.capability_hooks !== undefined) {
|
|
201
204
|
if (!Array.isArray(task.capability_hooks)) throw new Error(`${task.id} capability_hooks must be an array`);
|
|
202
205
|
for (const hook of task.capability_hooks) {
|
|
@@ -404,18 +407,21 @@ export function synthesizeCapability(slug, root = findRoot()) {
|
|
|
404
407
|
return { slug, status: 'synthesized', nodes: nodes.length, materials: materials.length };
|
|
405
408
|
}
|
|
406
409
|
|
|
407
|
-
export function confirmCapability(slug, options = {}, root = findRoot()) {
|
|
408
|
-
if (!options.scenario || options.scenario.length < 20) throw new Error('Confirm requires --scenario <text> (at least 20 characters describing which expert situation this project most resembles)');
|
|
410
|
+
export function confirmCapability(slug, options = {}, root = findRoot()) {
|
|
411
|
+
if (!options.scenario || options.scenario.length < 20) throw new Error('Confirm requires --scenario <text> (at least 20 characters describing which expert situation this project most resembles)');
|
|
412
|
+
if (!['human', 'agent'].includes(options.source)) throw new Error('Capability confirmation requires explicit provenance: --source human|agent');
|
|
409
413
|
const { paths } = loadProject(root);
|
|
410
414
|
const dir = join(paths.capabilities, slug);
|
|
411
415
|
if (!existsSync(dir)) throw new Error(`Capability not found: ${slug}`);
|
|
412
416
|
const statusPath = join(dir, 'status.json');
|
|
413
417
|
if (!existsSync(statusPath)) throw new Error(`Capability ${slug} is not a research-directory dossier`);
|
|
414
418
|
const status = readJson(statusPath, 'status.json');
|
|
415
|
-
if (status.status
|
|
416
|
-
status.scenario = options.scenario;
|
|
417
|
-
status.
|
|
418
|
-
status.
|
|
419
|
+
if (!['synthesized', 'provisional'].includes(status.status)) throw new Error(`Capability ${slug} must be synthesized before confirmation. Run loom capability synthesize ${slug} first.`);
|
|
420
|
+
status.scenario = options.scenario;
|
|
421
|
+
status.source = options.source;
|
|
422
|
+
status.status = options.source === 'human' ? 'confirmed' : 'provisional';
|
|
423
|
+
status.selected_at = now();
|
|
424
|
+
status.confirmed_at = options.source === 'human' ? now() : '';
|
|
419
425
|
status.updated_at = now();
|
|
420
426
|
atomicJson(statusPath, status);
|
|
421
427
|
const capabilityPath = join(dir, 'capability.md');
|
|
@@ -429,7 +435,7 @@ export function confirmCapability(slug, options = {}, root = findRoot()) {
|
|
|
429
435
|
const updated = content.replace(/## Project scenario\n[\s\S]*?\n## /, `${prefix}${blockquoteText}\n${options.scenario}\n${suffix}`);
|
|
430
436
|
writeFileSync(capabilityPath, updated, 'utf8');
|
|
431
437
|
}
|
|
432
|
-
return { slug, status:
|
|
438
|
+
return { slug, status: status.status, scenario: options.scenario, source: options.source };
|
|
433
439
|
}
|
|
434
440
|
|
|
435
441
|
export function getCapabilityStatus(slug, root = findRoot()) {
|
|
@@ -533,9 +539,12 @@ export function importTasks(payload, root = findRoot()) {
|
|
|
533
539
|
boundaries: raw.boundaries || [],
|
|
534
540
|
depends_on: raw.depends_on || [],
|
|
535
541
|
reads: raw.reads || ['.loom/PROJECT.md'],
|
|
536
|
-
touches: raw.touches || [],
|
|
537
|
-
implements: raw.implements || '',
|
|
538
|
-
|
|
542
|
+
touches: raw.touches || [],
|
|
543
|
+
implements: raw.implements || '',
|
|
544
|
+
design_exemption: raw.design_exemption || '',
|
|
545
|
+
capability_hooks: raw.capability_hooks || [],
|
|
546
|
+
capability_exemption: raw.capability_exemption || '',
|
|
547
|
+
integrity_version: 1,
|
|
539
548
|
covers: raw.covers || [],
|
|
540
549
|
status: 'open',
|
|
541
550
|
progress: raw.progress || { completed: [], current: '', next: '' },
|
|
@@ -566,18 +575,48 @@ export function getTask(id, root = findRoot()) {
|
|
|
566
575
|
return task;
|
|
567
576
|
}
|
|
568
577
|
|
|
569
|
-
function nextTask(tasks) {
|
|
578
|
+
function nextTask(tasks) {
|
|
570
579
|
const active = tasks.find((task) => task.status === 'active');
|
|
571
580
|
if (active) return active;
|
|
572
581
|
const done = new Set(tasks.filter((task) => task.status === 'done').map((task) => task.id));
|
|
573
582
|
return tasks.find((task) => task.status === 'open' && task.depends_on.every((id) => done.has(id))) || null;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function taskIntegrityFindings(task, designs, capabilities) {
|
|
586
|
+
if (task.integrity_version !== 1) return [];
|
|
587
|
+
const findings = [];
|
|
588
|
+
if (designs.length && !task.implements?.trim() && !task.design_exemption?.trim()) findings.push('is missing implements or design_exemption');
|
|
589
|
+
if (capabilities.length && !(task.capability_hooks || []).length && !task.capability_exemption?.trim()) findings.push('is missing capability_hooks or capability_exemption');
|
|
590
|
+
return findings;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function assertTaskIntegrityClassification(task, root) {
|
|
594
|
+
const findings = taskIntegrityFindings(task, listDesigns(root), listCapabilities(root));
|
|
595
|
+
if (findings.length) throw new Error(`${task.id} has unresolved integrity classification:\n- ${findings.join('\n- ')}\nAdd the relevant link or a concrete exemption before execution.`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function declaredArtifactPath(paths, ref) {
|
|
599
|
+
const normalized = ref.replaceAll('\\', '/');
|
|
600
|
+
if (isAbsolute(normalized) || normalized.startsWith('/') || normalized.includes('..')) throw new Error(`Unsafe declared artifact path: ${ref}`);
|
|
601
|
+
const fromLoom = normalized === '.loom' || normalized.startsWith('.loom/');
|
|
602
|
+
const base = fromLoom ? paths.loom : paths.root;
|
|
603
|
+
const relativeRef = fromLoom ? normalized.slice('.loom'.length).replace(/^\//, '') : normalized;
|
|
604
|
+
const absolute = resolve(base, relativeRef);
|
|
605
|
+
const prefix = `${base}${process.platform === 'win32' ? '\\' : '/'}`;
|
|
606
|
+
if (!absolute.startsWith(prefix) && absolute !== base) throw new Error(`Unsafe declared artifact path: ${ref}`);
|
|
607
|
+
return absolute;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function missingTaskOutputs(task, paths) {
|
|
611
|
+
if (task.integrity_version !== 1) return [];
|
|
612
|
+
return task.touches.filter((ref) => !existsSync(declaredArtifactPath(paths, ref)));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function updateTask(id, patch, root = findRoot()) {
|
|
577
616
|
const { paths, taskStore } = loadProject(root);
|
|
578
617
|
const task = taskStore.tasks.find((item) => item.id === id);
|
|
579
618
|
if (!task) throw new Error(`Task not found: ${id}`);
|
|
580
|
-
const allowed = ['title', 'outcome', 'acceptance', 'done_when', 'boundaries', 'depends_on', 'reads', 'touches', 'implements', 'capability_hooks', 'covers', 'progress', 'evidence'];
|
|
619
|
+
const allowed = ['title', 'outcome', 'acceptance', 'done_when', 'boundaries', 'depends_on', 'reads', 'touches', 'implements', 'design_exemption', 'capability_hooks', 'capability_exemption', 'covers', 'progress', 'evidence'];
|
|
581
620
|
for (const key of Object.keys(patch)) if (!allowed.includes(key)) throw new Error(`Task field cannot be updated: ${key}`);
|
|
582
621
|
Object.assign(task, patch, { updated_at: now() });
|
|
583
622
|
validateTasks(taskStore.tasks);
|
|
@@ -628,12 +667,13 @@ export function reopenTask(id, options = {}, root = findRoot()) {
|
|
|
628
667
|
return task;
|
|
629
668
|
}
|
|
630
669
|
|
|
631
|
-
export function startTask(id, root = findRoot()) {
|
|
632
|
-
const { paths, state, taskStore } = loadProject(root);
|
|
670
|
+
export function startTask(id, root = findRoot()) {
|
|
671
|
+
const { paths, state, taskStore } = loadProject(root);
|
|
633
672
|
if (!['passed', 'skipped'].includes(state.keeper.status)) throw new Error('The one-time Keeper handoff has not passed. Run loom keeper prompt.');
|
|
634
673
|
if (taskStore.tasks.some((task) => task.status === 'active')) throw new Error('Another Task is already active');
|
|
635
|
-
const task = taskStore.tasks.find((item) => item.id === id);
|
|
636
|
-
if (!task || task.status !== 'open') throw new Error(`${id} is not open`);
|
|
674
|
+
const task = taskStore.tasks.find((item) => item.id === id);
|
|
675
|
+
if (!task || task.status !== 'open') throw new Error(`${id} is not open`);
|
|
676
|
+
assertTaskIntegrityClassification(task, root);
|
|
637
677
|
const done = new Set(taskStore.tasks.filter((item) => item.status === 'done').map((item) => item.id));
|
|
638
678
|
const missing = task.depends_on.filter((dep) => !done.has(dep));
|
|
639
679
|
if (missing.length) throw new Error(`${id} has incomplete dependencies: ${missing.join(', ')}`);
|
|
@@ -648,12 +688,15 @@ export function startTask(id, root = findRoot()) {
|
|
|
648
688
|
return task;
|
|
649
689
|
}
|
|
650
690
|
|
|
651
|
-
export function completeTask(id, payload, root = findRoot()) {
|
|
691
|
+
export function completeTask(id, payload, root = findRoot()) {
|
|
652
692
|
const { paths, state, taskStore } = loadProject(root);
|
|
653
693
|
const task = taskStore.tasks.find((item) => item.id === id);
|
|
654
694
|
if (!task) throw new Error(`Task not found: ${id}`);
|
|
655
|
-
if (task.status !== 'active') throw new Error(`${id} is not active`);
|
|
656
|
-
if (!Array.isArray(payload.evidence) || !payload.evidence.length) throw new Error('Completing a Task requires concrete evidence');
|
|
695
|
+
if (task.status !== 'active') throw new Error(`${id} is not active`);
|
|
696
|
+
if (!Array.isArray(payload.evidence) || !payload.evidence.length) throw new Error('Completing a Task requires concrete evidence');
|
|
697
|
+
assertTaskIntegrityClassification(task, root);
|
|
698
|
+
const missingOutputs = missingTaskOutputs(task, paths);
|
|
699
|
+
if (missingOutputs.length) throw new Error(`${id} declared output does not exist:\n- ${missingOutputs.join('\n- ')}`);
|
|
657
700
|
const hasAcceptance = Array.isArray(task.acceptance) && task.acceptance.length > 0;
|
|
658
701
|
if (hasAcceptance) {
|
|
659
702
|
if (!Array.isArray(payload.acceptance_results)) throw new Error('Completing a Task with acceptance[] requires acceptance_results[]');
|
|
@@ -757,11 +800,15 @@ export function getKeeperPrompt(root = findRoot()) {
|
|
|
757
800
|
return keeperProtocol({ attemptNumber: state.keeper.prepared_attempt, preparedDigest: state.keeper.prepared_digest });
|
|
758
801
|
}
|
|
759
802
|
|
|
760
|
-
export function recordKeeper(payload, root = findRoot()) {
|
|
803
|
+
export function recordKeeper(payload, root = findRoot()) {
|
|
761
804
|
const { paths, state, taskStore } = loadProject(root);
|
|
762
805
|
if (state.project.status !== 'ready_for_keeper') throw new Error('Keeper result cannot be recorded before loom project ready');
|
|
763
|
-
if (!['passed', 'needs_revision', 'blocked'].includes(payload.verdict)) throw new Error('Keeper verdict must be passed, needs_revision, or blocked');
|
|
764
|
-
if (!payload.summary || !Array.isArray(payload.evidence) || !payload.evidence.length) throw new Error('Keeper result requires summary and evidence');
|
|
806
|
+
if (!['passed', 'needs_revision', 'blocked'].includes(payload.verdict)) throw new Error('Keeper verdict must be passed, needs_revision, or blocked');
|
|
807
|
+
if (!payload.summary || !Array.isArray(payload.evidence) || !payload.evidence.length) throw new Error('Keeper result requires summary and evidence');
|
|
808
|
+
if (payload.verdict === 'passed') {
|
|
809
|
+
if (!payload.review || payload.review.mode !== 'independent') throw new Error('Keeper pass requires an independent review; use review.mode="independent" from a fresh Agent, or loom keeper skip with a concrete reason');
|
|
810
|
+
if (!payload.review.reviewer_id || payload.review.reviewer_id.length < 6 || !payload.review.evidence || payload.review.evidence.length < 10) throw new Error('Independent Keeper review requires reviewer_id and concrete review evidence');
|
|
811
|
+
}
|
|
765
812
|
if (payload.evidence.some((item) => typeof item !== 'string' || !item.trim())) throw new Error('Keeper evidence entries must be non-empty strings');
|
|
766
813
|
if (payload.gaps !== undefined && !Array.isArray(payload.gaps)) throw new Error('Keeper gaps must be an array');
|
|
767
814
|
for (const gap of payload.gaps || []) {
|
|
@@ -784,7 +831,7 @@ export function recordKeeper(payload, root = findRoot()) {
|
|
|
784
831
|
if (!payload.prepared_digest || payload.prepared_digest !== state.keeper.prepared_digest) throw new Error('Keeper result prepared_digest does not match the current ready state');
|
|
785
832
|
const currentDigest = projectDigest(paths, taskStore.tasks);
|
|
786
833
|
if (currentDigest !== state.keeper.prepared_digest) throw new Error('Project truth changed after loom project ready; prepare a new Keeper attempt');
|
|
787
|
-
const attempt = { run_id: payload.run_id, prepared_digest: payload.prepared_digest, verdict: payload.verdict, summary: payload.summary, evidence: payload.evidence, gaps: payload.gaps || [], can_auto_pass: canAutoPass, at: now() };
|
|
834
|
+
const attempt = { run_id: payload.run_id, prepared_digest: payload.prepared_digest, verdict: payload.verdict, summary: payload.summary, evidence: payload.evidence, gaps: payload.gaps || [], review: payload.review || { mode: 'unverified', reviewer_id: '', evidence: '' }, can_auto_pass: canAutoPass, at: now() };
|
|
788
835
|
state.keeper.attempts.push(attempt);
|
|
789
836
|
state.keeper.status = payload.verdict;
|
|
790
837
|
if (payload.verdict === 'passed') state.project.status = 'build_ready';
|
|
@@ -818,7 +865,7 @@ export function recordDecision(payload, root = findRoot()) {
|
|
|
818
865
|
return { id, summary: payload.summary, changes: payload.changes, affected_tasks: payload.affected_tasks || [] };
|
|
819
866
|
}
|
|
820
867
|
|
|
821
|
-
export function compileContext(options = {}, root = findRoot()) {
|
|
868
|
+
export function compileContext(options = {}, root = findRoot()) {
|
|
822
869
|
const { paths, state, taskStore } = loadProject(root);
|
|
823
870
|
const designs = listDesigns(root);
|
|
824
871
|
const capabilities = listCapabilities(root);
|
|
@@ -834,8 +881,12 @@ export function compileContext(options = {}, root = findRoot()) {
|
|
|
834
881
|
: state.project.status === 'shaping'
|
|
835
882
|
? 'Project is still shaping. Confirm the intended result, identify open questions, and build the Work Map before starting material work.'
|
|
836
883
|
: 'No executable Task. Create or update Tasks so the Work Map matches the project goal.';
|
|
837
|
-
const
|
|
838
|
-
|
|
884
|
+
const capabilityStates = capabilities.map((name) => {
|
|
885
|
+
const statusPath = join(paths.capabilities, name, 'status.json');
|
|
886
|
+
return existsSync(statusPath) ? `${name} (${readJson(statusPath, 'status.json').status || 'unknown'})` : `${name} (legacy)`;
|
|
887
|
+
});
|
|
888
|
+
const statusBlock = `## Current LOOM state and recommended action\n\n- Project status: ${state.project.status}\n- Active task: ${summary.active || 'none'}\n- Work map: ${summary.total} total, ${summary.open} open, ${summary.done} done, ${summary.blocked} blocked\n- Design documents: ${designs.length}\n- Capability dossiers: ${capabilityStates.length ? capabilityStates.join(', ') : 'none'}\n- Keeper status: ${state.keeper.status}\n\n**Recommended next action:** ${recommendation}\n\nThis is a recommendation, not a script. Use your judgment; if you choose differently, record the reason in \`.loom/DECISIONS.md\` or the active Task evidence.`;
|
|
889
|
+
const blocks = [statusBlock, agentProtocol({ humanChannel: options.humanChannel || 'available' }), shapingContext({ state, taskSummary: summary, capabilityNames: capabilityStates, designNames: designs, forKeeper: Boolean(options.keeper) })];
|
|
839
890
|
blocks.push(`## Project whole (${normalizeRef(paths, paths.project)})\n\n${readFileSync(paths.project, 'utf8')}`);
|
|
840
891
|
if (existsSync(paths.structure)) blocks.push(`## Project structure (${normalizeRef(paths, paths.structure)})\n\n${readFileSync(paths.structure, 'utf8')}`);
|
|
841
892
|
if (options.keeper) {
|
|
@@ -889,20 +940,31 @@ function readContextDocument(paths, ref) {
|
|
|
889
940
|
return readFileSync(absolute, 'utf8');
|
|
890
941
|
}
|
|
891
942
|
|
|
892
|
-
export function checkProject(root = findRoot()) {
|
|
893
|
-
const { paths, state, taskStore } = loadProject(root);
|
|
894
|
-
const
|
|
895
|
-
const
|
|
943
|
+
export function checkProject(root = findRoot()) {
|
|
944
|
+
const { paths, state, taskStore } = loadProject(root);
|
|
945
|
+
const designs = listDesigns(root);
|
|
946
|
+
const capabilities = listCapabilities(root);
|
|
947
|
+
const errors = [];
|
|
948
|
+
const warnings = [];
|
|
896
949
|
for (const task of taskStore.tasks) {
|
|
897
950
|
for (const ref of task.reads) {
|
|
898
951
|
try { readContextDocument(paths, ref); } catch (error) { errors.push(`${task.id}: ${error.message}`); }
|
|
899
952
|
}
|
|
900
|
-
if (task.capability_hooks) {
|
|
953
|
+
if (task.capability_hooks) {
|
|
901
954
|
for (const hook of task.capability_hooks) {
|
|
902
955
|
const nodeContent = extractCapabilityNode(paths, hook.node);
|
|
903
956
|
if (nodeContent === null) warnings.push(`${task.id} references missing capability node: ${hook.node}`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (task.integrity_version === 1) {
|
|
960
|
+
const classificationFindings = taskIntegrityFindings(task, designs, capabilities);
|
|
961
|
+
const target = ['active', 'done'].includes(task.status) ? errors : warnings;
|
|
962
|
+
target.push(...classificationFindings.map((finding) => `${task.id} ${finding}`));
|
|
963
|
+
if (task.status === 'done') {
|
|
964
|
+
const missingOutputs = missingTaskOutputs(task, paths);
|
|
965
|
+
errors.push(...missingOutputs.map((output) => `${task.id} declared output does not exist: ${output}`));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
906
968
|
const hasAcceptance = Array.isArray(task.acceptance) && task.acceptance.length > 0;
|
|
907
969
|
const hasDoneWhen = Array.isArray(task.done_when) && task.done_when.length > 0;
|
|
908
970
|
if (!hasAcceptance && hasDoneWhen) warnings.push(`${task.id} uses done_when[] without acceptance[] — consider migrating to structured acceptance for clearer verification`);
|
|
@@ -919,23 +981,25 @@ export function checkProject(root = findRoot()) {
|
|
|
919
981
|
const task = taskStore.tasks.find((item) => item.id === taskId);
|
|
920
982
|
if (task && task.status === 'done') warnings.push(`${taskId} is done but was marked affected by a decision; consider reopening if the change invalidates prior work`);
|
|
921
983
|
}
|
|
922
|
-
if (!
|
|
923
|
-
if (!
|
|
984
|
+
if (!capabilities.length) warnings.push('No capability dossier exists; acceptable only when specialist judgment would not change the work');
|
|
985
|
+
if (!designs.length) warnings.push('No design document exists; PROJECT.md should remain a concise map of the whole');
|
|
924
986
|
if (!existsSync(paths.structure)) warnings.push('No STRUCTURE.md exists; declare where files go so the Agent does not guess');
|
|
925
987
|
else if (readFileSync(paths.structure, 'utf8').includes('Where implementation files go. Example:')) warnings.push('STRUCTURE.md still contains template instructions; customize it for this project');
|
|
926
|
-
for (const name of
|
|
988
|
+
for (const name of designs) {
|
|
927
989
|
if (readFileSync(join(paths.design, name), 'utf8').includes('Describe the project-specific decision, mechanism, boundary, or evidence owned by this section.')) warnings.push(`Design document still contains template instructions: ${name}`);
|
|
928
990
|
}
|
|
929
|
-
for (const name of
|
|
991
|
+
for (const name of capabilities) {
|
|
930
992
|
const content = readFileSync(capabilityPath(paths, name), 'utf8');
|
|
931
993
|
if (capabilityTemplateResidue(content)) warnings.push(`Capability dossier still contains template instructions: ${name}`);
|
|
932
994
|
if (content.includes('### C') && !content.includes('source:')) warnings.push(`Capability dossier has decision tree nodes without source citations: ${name}`);
|
|
933
995
|
const statusPath = join(paths.capabilities, name, 'status.json');
|
|
934
996
|
if (existsSync(statusPath)) {
|
|
935
997
|
const capStatus = readJson(statusPath, 'status.json');
|
|
936
|
-
if (capStatus.status && capStatus.status !== 'confirmed') warnings.push(`Capability ${name} is ${capStatus.status}, not confirmed; tasks referencing it proceed provisionally`);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
998
|
+
if (capStatus.status && capStatus.status !== 'confirmed') warnings.push(`Capability ${name} is ${capStatus.status}, not confirmed; tasks referencing it proceed provisionally`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const latestKeeper = state.keeper.attempts.at(-1);
|
|
1002
|
+
if (state.keeper.status === 'passed' && latestKeeper && latestKeeper.review?.mode !== 'independent') warnings.push('Keeper pass has no independently attested review provenance; prepare a fresh review before relying on it');
|
|
939
1003
|
const coverage = checkDeliverableCoverage(root);
|
|
940
1004
|
if (coverage.uncovered > 0) warnings.push(`Uncovered deliverables: ${coverage.uncovered_items.map((item) => item.slug).join(', ')}`);
|
|
941
1005
|
return { healthy: errors.length === 0, errors, warnings, summary: taskSummary(taskStore.tasks), deliverable_coverage: { total: coverage.total, covered: coverage.covered, uncovered: coverage.uncovered } };
|
package/design.md
CHANGED
|
@@ -75,8 +75,12 @@ Cross-field synthesis lives in the design document whose decision it changes. A
|
|
|
75
75
|
- generic failure modes and observable verification signals;
|
|
76
76
|
- opened sources, what each changed, conflicts, and uncertainty when research is used.
|
|
77
77
|
|
|
78
|
-
Acquisition can happen quietly. The Agent may briefly name the capability it is obtaining, but does not
|
|
79
|
-
turn research logistics into user workflow.
|
|
78
|
+
Acquisition can happen quietly. The Agent may briefly name the capability it is obtaining, but does not
|
|
79
|
+
turn research logistics into user workflow.
|
|
80
|
+
|
|
81
|
+
Scenario authority is explicit. `capability confirm --source human` records a confirmed scenario;
|
|
82
|
+
`--source agent` records a provisional, reversible selection when the human is unavailable. Context and
|
|
83
|
+
health checks preserve that distinction instead of allowing an Agent assumption to masquerade as user confirmation.
|
|
80
84
|
|
|
81
85
|
### Work Map and Task
|
|
82
86
|
|
|
@@ -94,8 +98,10 @@ Task is the single execution contract:
|
|
|
94
98
|
"done_when": ["A forced-reset transcript resumes correctly"],
|
|
95
99
|
"boundaries": ["The human never operates LOOM"],
|
|
96
100
|
"depends_on": [],
|
|
97
|
-
"reads": [".loom/PROJECT.md", ".loom/design/context-system.md", ".loom/capabilities/human-agent-interaction.md", "fixtures/reset-case.md"],
|
|
98
|
-
"touches": ["cli/src/context.js"],
|
|
101
|
+
"reads": [".loom/PROJECT.md", ".loom/design/context-system.md", ".loom/capabilities/human-agent-interaction.md", "fixtures/reset-case.md"],
|
|
102
|
+
"touches": ["cli/src/context.js"],
|
|
103
|
+
"implements": ".loom/design/context-system.md#Context selection",
|
|
104
|
+
"capability_hooks": [{ "node": "human-agent-interaction#C1", "at": "selecting reset context" }],
|
|
99
105
|
"status": "active",
|
|
100
106
|
"progress": {
|
|
101
107
|
"completed": [],
|
|
@@ -106,17 +112,23 @@ Task is the single execution contract:
|
|
|
106
112
|
}
|
|
107
113
|
```
|
|
108
114
|
|
|
109
|
-
There is at most one active Task. Completion requires a concrete evidence mapping for every exact `done_when`
|
|
110
|
-
criterion. A completed Task can be reopened with a reason when later evidence disproves it. The Task plan may change without
|
|
111
|
-
human approval when the change is a reversible implementation refinement inside the agreed whole; outcome,
|
|
112
|
-
authority, risk, or material cost changes return to the conversation.
|
|
115
|
+
There is at most one active Task. Completion requires a concrete evidence mapping for every exact `done_when`
|
|
116
|
+
criterion. A completed Task can be reopened with a reason when later evidence disproves it. The Task plan may change without
|
|
117
|
+
human approval when the change is a reversible implementation refinement inside the agreed whole; outcome,
|
|
118
|
+
authority, risk, or material cost changes return to the conversation.
|
|
119
|
+
|
|
120
|
+
New Tasks must make design and capability applicability explicit: use `implements` and `capability_hooks`, or
|
|
121
|
+
record a concrete `design_exemption` / `capability_exemption`. Task start rejects an unclassified active horizon.
|
|
122
|
+
Completion verifies that every declared local `touches` path exists, and `loom check` detects later filesystem drift.
|
|
113
123
|
|
|
114
124
|
### One-time Keeper
|
|
115
125
|
|
|
116
126
|
Keeper is not a recurring role. It is a single isolation test at the transition from shaping to material
|
|
117
127
|
execution. A fresh Agent receives no prior conversation, runs `loom context --keeper`, explains the whole,
|
|
118
128
|
selects a first Task, navigates its design documents and professional capabilities, and identifies concrete evidence. Every attempt binds a unique
|
|
119
|
-
fresh-thread `run_id` to the digest frozen by `project ready`.
|
|
129
|
+
fresh-thread `run_id` to the digest frozen by `project ready`. A passing record also carries an explicit independent
|
|
130
|
+
review attestation (`review.mode`, reviewer identity, and isolation evidence); known self-review cannot be recorded as
|
|
131
|
+
passed. This is an auditable host assertion, not cryptographic identity proof. It may return gaps; summary, evidence and exact gaps
|
|
120
132
|
are injected into the main Agent's next context. Revision must change project truth or Task definitions before a
|
|
121
133
|
new digest and fresh Keeper run are allowed. Once passed, normal Task evidence replaces further Keeper ceremony.
|
|
122
134
|
|
|
@@ -132,15 +144,15 @@ cli/src/protocol.js layered Agent prompts and every human-readable template
|
|
|
132
144
|
cli/test/run-all.js end-to-end contract tests
|
|
133
145
|
```
|
|
134
146
|
|
|
135
|
-
The CLI has six responsibilities:
|
|
147
|
+
The CLI has six responsibilities:
|
|
136
148
|
|
|
137
149
|
1. Initialize the minimal project backbone.
|
|
138
150
|
2. Record structured understanding and decision history safely.
|
|
139
151
|
3. Scaffold a scalable design-document graph and separate professional-field dossiers.
|
|
140
|
-
4. Maintain a large Work Map and a single active Task.
|
|
141
|
-
5. Compile only decision-relevant context after reset.
|
|
142
|
-
6. Adapt clarification to whether a human response channel is actually available.
|
|
143
|
-
7. Prepare one-time Keeper and controlled Evil Eval artifacts.
|
|
152
|
+
4. Maintain a large Work Map and a single active Task.
|
|
153
|
+
5. Compile only decision-relevant context after reset.
|
|
154
|
+
6. Adapt clarification to whether a human response channel is actually available.
|
|
155
|
+
7. Prepare one-time Keeper and controlled Evil Eval artifacts.
|
|
144
156
|
|
|
145
157
|
It does not orchestrate models, browse the web, generate project documents, or pretend that a CLI command
|
|
146
158
|
clears model memory. Those capabilities belong to the host Agent and its tools.
|
|
@@ -157,42 +169,43 @@ ready_for_keeper ⇄ fresh Keeper needs_revision or blocked → shaping
|
|
|
157
169
|
build_ready → building ⇄ Task repair or block or reopen → complete
|
|
158
170
|
```
|
|
159
171
|
|
|
160
|
-
`project ready` checks only structural prerequisites: PROJECT.md is no longer a template, at least one design
|
|
161
|
-
document and a Work Map exist, and no high-impact open question remains. Keeper judges semantic build-readiness. Task start requires Keeper
|
|
162
|
-
pass or an explicit recorded skip. An unchanged revision, duplicate Keeper run, wrong digest, or stale prepared
|
|
163
|
-
state is rejected. Imported Tasks start open, `task start` rejects missing or directory-level context reads
|
|
164
|
-
generic updates cannot change status, and completion requires
|
|
172
|
+
`project ready` checks only structural prerequisites: PROJECT.md is no longer a template, at least one design
|
|
173
|
+
document and a Work Map exist, and no high-impact open question remains. Keeper judges semantic build-readiness. Task start requires Keeper
|
|
174
|
+
pass or an explicit recorded skip. An unchanged revision, duplicate Keeper run, wrong digest, or stale prepared
|
|
175
|
+
state is rejected. Imported Tasks start open, `task start` rejects missing or directory-level context reads and
|
|
176
|
+
unclassified design/capability applicability, generic updates cannot change status, and completion requires
|
|
177
|
+
criterion-by-criterion evidence plus existence of every declared local output.
|
|
165
178
|
|
|
166
179
|
## Context selection
|
|
167
180
|
|
|
168
|
-
`loom context` always injects:
|
|
181
|
+
`loom context` always injects:
|
|
169
182
|
|
|
170
183
|
- the stable Agent protocol;
|
|
171
184
|
- compact state counts and open uncertainty;
|
|
172
185
|
- PROJECT.md, with decision history identified as on-demand context;
|
|
173
|
-
- the active Task, if one exists;
|
|
174
|
-
- the exact design, capability, source, contract, or fixture files named in that Task's `reads` list.
|
|
175
|
-
|
|
176
|
-
### Environment adaptation and isolated state
|
|
177
|
-
|
|
178
|
-
The host supplies human-channel availability at context time. With `available` (the default), the Agent may
|
|
179
|
-
ask the human for consequential intent, preference, authority, or facts that only they hold. With
|
|
180
|
-
`unavailable`, it must not fabricate a user exchange or replace an intent/authority question with web search.
|
|
181
|
-
It first inspects discoverable workspace facts and permitted tools, researches only objective external facts when
|
|
182
|
-
the task allows it, then records a bounded assumption and chooses a safe reversible action or blocks on
|
|
183
|
-
unavailable authority.
|
|
184
|
-
|
|
185
|
-
`--state-dir <outside-workspace-dir>` places LOOM's state tree in a per-run sidecar rather than the workspace.
|
|
186
|
-
This is for benchmark or sandbox runners whose scored workspace must remain pristine. The sidecar remains
|
|
187
|
-
semantically `.loom/` to LOOM documents and Task references, but initialization does not edit the workspace
|
|
188
|
-
or add an AGENTS.md anchor. Every command in that run must receive the same state directory.
|
|
189
|
-
|
|
190
|
-
When a Task is active, context also injects a short execution protocol. It tells a reset Agent to reconcile
|
|
191
|
-
the Task with the current workspace and version-control state, inspect relevant tests before editing, choose
|
|
192
|
-
verification according to risk and the exact `done_when` claims, persist `completed/current/next` at meaningful
|
|
193
|
-
handoff boundaries, and close only with reproducible criterion-level evidence. It deliberately does not require
|
|
194
|
-
a ceremonial unit test for every kind of work or a branch, commit, or pull request for every Task. Those
|
|
195
|
-
delivery mechanisms remain conditional on the human request and repository workflow.
|
|
186
|
+
- the active Task, if one exists;
|
|
187
|
+
- the exact design, capability, source, contract, or fixture files named in that Task's `reads` list.
|
|
188
|
+
|
|
189
|
+
### Environment adaptation and isolated state
|
|
190
|
+
|
|
191
|
+
The host supplies human-channel availability at context time. With `available` (the default), the Agent may
|
|
192
|
+
ask the human for consequential intent, preference, authority, or facts that only they hold. With
|
|
193
|
+
`unavailable`, it must not fabricate a user exchange or replace an intent/authority question with web search.
|
|
194
|
+
It first inspects discoverable workspace facts and permitted tools, researches only objective external facts when
|
|
195
|
+
the task allows it, then records a bounded assumption and chooses a safe reversible action or blocks on
|
|
196
|
+
unavailable authority.
|
|
197
|
+
|
|
198
|
+
`--state-dir <outside-workspace-dir>` places LOOM's state tree in a per-run sidecar rather than the workspace.
|
|
199
|
+
This is for benchmark or sandbox runners whose scored workspace must remain pristine. The sidecar remains
|
|
200
|
+
semantically `.loom/` to LOOM documents and Task references, but initialization does not edit the workspace
|
|
201
|
+
or add an AGENTS.md anchor. Every command in that run must receive the same state directory.
|
|
202
|
+
|
|
203
|
+
When a Task is active, context also injects a short execution protocol. It tells a reset Agent to reconcile
|
|
204
|
+
the Task with the current workspace and version-control state, inspect relevant tests before editing, choose
|
|
205
|
+
verification according to risk and the exact `done_when` claims, persist `completed/current/next` at meaningful
|
|
206
|
+
handoff boundaries, and close only with reproducible criterion-level evidence. It deliberately does not require
|
|
207
|
+
a ceremonial unit test for every kind of work or a branch, commit, or pull request for every Task. Those
|
|
208
|
+
delivery mechanisms remain conditional on the human request and repository workflow.
|
|
196
209
|
|
|
197
210
|
Workspace code paths in `touches` are identified but not automatically copied into context. Full decision
|
|
198
211
|
history is not repeatedly injected because current truth belongs in PROJECT.md and linked design documents.
|
|
@@ -209,7 +222,8 @@ an automated semantic migration can be designed only after real v1 projects reve
|
|
|
209
222
|
|
|
210
223
|
- External capability acquisition is represented but not automated yet; the host Agent performs research.
|
|
211
224
|
- Markdown is intentionally human-editable. LOOM validates file existence and structured state, not prose truth.
|
|
212
|
-
- Keeper independence depends on the host creating a fresh thread or window.
|
|
225
|
+
- Keeper independence depends on the host creating a fresh thread or window. LOOM requires and records the
|
|
226
|
+
host's independent-review attestation but cannot cryptographically prove Agent identity.
|
|
213
227
|
- Evil Eval scaffolding controls experiment design but does not itself launch model runs.
|
|
214
228
|
- A one-time Keeper verifies build-readiness, not the eventual quality of every implementation Task.
|
|
215
229
|
|