@haaaiawd/loom 2.1.1 → 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 CHANGED
@@ -1,10 +1,17 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
4
-
5
- - Added environment-aware clarification through `loom context --human-channel available|unavailable`. Unattended Agents inspect first, research only permitted objective facts, and then use bounded assumptions or blocks rather than fabricating users or substituting web search for intent.
6
- - Added `--state-dir <outside-workspace-dir>` sidecars for isolated benchmark and sandbox runs. Sidecars preserve virtual `.loom/` Task references without mutating the scored workspace or its `AGENTS.md`.
7
- - Added equal human-channel controls and unattended condition prompts to Evil Eval scaffolding.
3
+ ## Unreleased
4
+
5
+ ## 2.1.2
6
+
7
+ - Added environment-aware clarification through `loom context --human-channel available|unavailable`. Unattended Agents inspect first, research only permitted objective facts, and then use bounded assumptions or blocks rather than fabricating users or substituting web search for intent.
8
+ - Added `--state-dir <outside-workspace-dir>` sidecars for isolated benchmark and sandbox runs. Sidecars preserve virtual `.loom/` Task references without mutating the scored workspace or its `AGENTS.md`.
9
+ - Added equal human-channel controls and unattended condition prompts to Evil Eval scaffolding.
10
+ - Added explicit capability authority provenance: human selections become confirmed, while Agent selections remain visibly provisional until human confirmation.
11
+ - Required independent Keeper review provenance for a passing handoff; known self-review can no longer be recorded as an independent pass.
12
+ - Added Task integrity enforcement for design links, capability hooks or explicit exemptions, plus declared output existence checks before completion.
13
+ - Added command-specific canonical JSON help and recovery pointers for every structured-write command.
14
+ - Updated generated Agent guidance and both public READMEs so installed and source-checkout invocation paths are unambiguous.
8
15
 
9
16
  ## 2.1.1
10
17
 
package/README.md CHANGED
@@ -12,9 +12,9 @@
12
12
 
13
13
  <p align="center"><strong>The human talks to the Agent. LOOM helps the Agent remember, understand, and continue.</strong></p>
14
14
 
15
- LOOM is invisible project-understanding and continuity infrastructure for AI Agents. It supports
16
- any work that can be materially advanced through code or command-line tools: software, operations,
17
- research workflows, office automation, data work, and highly personalized projects.
15
+ LOOM is spec-driven continuity, proof, and project understanding infrastructure for AI coding agents.
16
+ It supports any work that can be materially advanced through code or command-line tools: software,
17
+ operations, research workflows, office automation, data work, and highly personalized projects.
18
18
 
19
19
  The human does not learn a framework or operate a CLI. They describe what they want, answer useful
20
20
  questions, disagree, delegate decisions, and watch the work become real. The Agent uses LOOM in the
@@ -65,10 +65,13 @@ understand or verify it independently. `PROJECT.md` maps the whole instead of be
65
65
 
66
66
  A dossier exists only when specialist knowledge would change a question, design choice,
67
67
  implementation, risk, or verification method. Each dossier represents one recognizable professional
68
- field—such as UI/UX design, visual art direction, game design, psychology, security, or distributed
69
- systems. Different fields remain separate even when tightly coupled; their synthesis belongs in the
70
- design document whose decision they jointly shape. A task technique such as triage, ranking, parsing,
71
- or caching is not allowed to masquerade as the project's entire capability surface.
68
+ field—such as UI/UX design, visual art direction, game design, psychology, security, or distributed
69
+ systems. Different fields remain separate even when tightly coupled; their synthesis belongs in the
70
+ design document whose decision they jointly shape. A task technique such as triage, ranking, parsing,
71
+ or caching is not allowed to masquerade as the project's entire capability surface.
72
+
73
+ Capability scenario authority remains visible: `--source human` confirms a user-selected scenario;
74
+ `--source agent` records a provisional selection when the human is unavailable.
72
75
 
73
76
  ### Work Map and Task
74
77
 
@@ -82,7 +85,8 @@ Agent:
82
85
  - what observable result to create;
83
86
  - what proves completion;
84
87
  - what must not be damaged;
85
- - which project and capability documents matter;
88
+ - which project and capability documents matter;
89
+ - which design decision and capability nodes apply, or why they do not;
86
90
  - what has happened, what is happening, and what comes next;
87
91
  - which evidence already exists and which exact done condition it proves.
88
92
 
@@ -144,8 +148,9 @@ loom design add product --title "Product definition" --kind product
144
148
  loom design add local-analysis --title "Local analysis system" --kind system
145
149
  loom design add acceptance --title "Vertical-slice verification" --kind verification
146
150
  loom capability add ui-ux-design --title "UI/UX design"
147
- loom capability add behavioral-psychology --title "Behavioral psychology"
148
- loom task plan --json-file initial-work-map.json
151
+ loom capability add behavioral-psychology --title "Behavioral psychology"
152
+ loom capability confirm behavioral-psychology --scenario "<project-specific expert situation>" --source human
153
+ loom task plan --json-file initial-work-map.json
149
154
  loom project ready
150
155
  ```
151
156
 
@@ -155,7 +160,11 @@ At the transition to material execution, open a fresh Agent thread and give it o
155
160
  Run loom keeper prompt in this project and follow it. Decide whether you can responsibly start.
156
161
  ```
157
162
 
158
- If Keeper returns `needs_revision` or `blocked`, those exact gaps reappear in `loom context`. The Agent
163
+ Keeper pass JSON records `review.mode: "independent"`, a reviewer identity, and concrete isolation evidence.
164
+ Known self-review cannot pass; if the host cannot isolate a fresh Agent, use `loom keeper skip` with the
165
+ reason so reduced confidence stays visible.
166
+
167
+ If Keeper returns `needs_revision` or `blocked`, those exact gaps reappear in `loom context`. The Agent
159
168
  repairs the relevant project, design, capability, or Task source, prepares a changed digest, and opens a
160
169
  different fresh Keeper. If the host cannot create a subagent, the human can open a new window and use the
161
170
  same sentence. After the one-time handoff passes, the execution Agent starts and maintains Tasks normally:
@@ -175,17 +184,19 @@ Completion is deliberately explicit:
175
184
 
176
185
  ```json
177
186
  {
178
- "evidence": ["npm test: 20 passed, 0 failed"],
187
+ "evidence": ["npm test: 21 passed, 0 failed"],
179
188
  "acceptance_results": [
180
189
  {
181
190
  "criterion": "The exact acceptance criterion from the Task.",
182
- "evidence": ["The command, artifact, or observation that proves this criterion."]
191
+ "evidence": "The command, artifact, or observation that proves this criterion."
183
192
  }
184
193
  ]
185
194
  }
186
195
  ```
187
196
 
188
- Run `loom --help` for the complete command surface. Run `loom check` for structural health. Run
197
+ Run `loom --help` for the complete command surface. Structured-write commands expose canonical payloads
198
+ through command-specific help such as `loom record --help` and `loom task done --help`. Run `loom check`
199
+ for structural health. Run
189
200
  `loom prompts` to print every cognitive message LOOM can inject: the stable collaboration core,
190
201
  runtime protocol, dynamic state layer, all document templates, Keeper prompt, eval conditions, judge
191
202
  prompt, and their composition order. See the [prompt and message catalog](docs/PROMPT_CATALOG.md).
@@ -215,7 +226,7 @@ user burden, time, and token cost are penalized alongside quality. See [EVIL_EVA
215
226
  npm test
216
227
  ```
217
228
 
218
- The v2 test suite (20 end-to-end tests) exercises the complete loop, including a 250-Task Work Map, context selection,
229
+ The v2 test suite exercises the complete loop, including a 250-Task Work Map, context selection,
219
230
  superseding decisions, scalable design documents, professional-field separation, capability compilation with
220
231
  source-citation validation, multi-attempt Keeper revision with auto-pass, stale digest and duplicate-run
221
232
  rejection, exact-file Task start, block/reopen including disproven completion, per-acceptance-criterion evidence,
package/README.zh-CN.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  <p align="center"><strong>人只需要和 Agent 自然聊天。LOOM 在背后帮助 Agent 记住、理解,并继续工作。</strong></p>
14
14
 
15
- LOOM 是为 AI Agent 准备的、隐形的项目理解与连续性基础设施。凡是能够通过代码或命令行获得实质推进的工作——软件、运维、科研流程、办公自动化、数据处理,以及高度个性化的项目——都在它的能力边界内。
15
+ LOOM 是为 AI 编码 Agent 准备的 spec 驱动连续性、证据与项目理解基础设施。凡是能够通过代码或命令行获得实质推进的工作——软件、运维、科研流程、办公自动化、数据处理,以及高度个性化的项目——都在它的能力边界内。
16
16
 
17
17
  用户不需要学习一套框架,也不需要亲自操作 CLI。他只需要描述想要什么、回答真正有价值的问题、提出异议、把某些判断交给 Agent,然后看着结果逐渐成为现实。Agent 在后台使用 LOOM,让项目全貌在上下文压缩、会话切换和长期施工中保持连续。
18
18
 
@@ -115,19 +115,22 @@ loom record --json-file understanding-update.json
115
115
  loom design add product --title "Product definition" --kind product
116
116
  loom design add local-analysis --title "Local analysis system" --kind system
117
117
  loom design add acceptance --title "Vertical-slice verification" --kind verification
118
- loom capability add ui-ux-design --title "UI/UX design"
119
- loom capability add behavioral-psychology --title "Behavioral psychology"
120
- loom task plan --json-file initial-work-map.json
121
- loom project ready
122
- ```
118
+ loom capability add ui-ux-design --title "UI/UX design"
119
+ loom capability add behavioral-psychology --title "Behavioral psychology"
120
+ loom capability confirm behavioral-psychology --scenario "<项目特定的专家情境>" --source human
121
+ loom task plan --json-file initial-work-map.json
122
+ loom project ready
123
+ ```
123
124
 
124
125
  准备从项目成型进入实质施工时,打开一个全新的 Agent 线程,只给它一句简短指令:
125
126
 
126
127
  ```text
127
- Run loom keeper prompt in this project and follow it. Decide whether you can responsibly start.
128
- ```
129
-
130
- 如果 Keeper 返回 `needs_revision` 或 `blocked`,这些具体缺口会重新出现在 `loom context` 中。Agent 修复对应的项目、设计、能力或 Task 源文件,生成发生变化的新 digest,再打开另一个全新的 Keeper。若宿主不支持子代理,用户可以新开一个窗口并使用同一句提示词。
128
+ Run loom keeper prompt in this project and follow it. Decide whether you can responsibly start.
129
+ ```
130
+
131
+ Keeper 通过记录必须包含 `review.mode: "independent"`、审查者标识和具体的隔离证据。已知的自我审查不能通过;如果宿主无法隔离出全新的 Agent,应使用 `loom keeper skip` 并写明原因,让置信度降低这件事保持可见。
132
+
133
+ 如果 Keeper 返回 `needs_revision` 或 `blocked`,这些具体缺口会重新出现在 `loom context` 中。Agent 修复对应的项目、设计、能力或 Task 源文件,生成发生变化的新 digest,再打开另一个全新的 Keeper。若宿主不支持子代理,用户可以新开一个窗口并使用同一句提示词。
131
134
 
132
135
  一次性交接通过后,施工 Agent 正常启动并维护 Task:
133
136
 
@@ -146,17 +149,17 @@ loom task done TASK-001 --json-file evidence.json
146
149
 
147
150
  ```json
148
151
  {
149
- "evidence": ["npm test: 20 passed, 0 failed"],
152
+ "evidence": ["npm test: 21 passed, 0 failed"],
150
153
  "acceptance_results": [
151
- {
152
- "criterion": "The exact acceptance criterion from the Task.",
153
- "evidence": ["The command, artifact, or observation that proves this criterion."]
154
- }
155
- ]
156
- }
157
- ```
158
-
159
- 运行 `loom --help` 查看全部命令,运行 `loom check` 检查结构健康度。`loom prompts` 会打印 LOOM 可能注入的全部认知消息:稳定协作核心、运行时协议、动态状态层、所有文档模板、Keeper 提示词、Eval 条件、裁判提示词,以及它们的组合顺序。详见[提示词与消息目录](docs/PROMPT_CATALOG.md)。
154
+ {
155
+ "criterion": "The exact acceptance criterion from the Task.",
156
+ "evidence": "The command, artifact, or observation that proves this criterion."
157
+ }
158
+ ]
159
+ }
160
+ ```
161
+
162
+ 运行 `loom --help` 查看全部命令。所有结构化写入命令都通过命令级帮助给出可直接使用的标准 JSON,例如 `loom record --help` 和 `loom task done --help`;校验失败也会保留具体错误,并指向对应帮助。运行 `loom check` 检查结构健康度。`loom prompts` 会打印 LOOM 可能注入的全部认知消息:稳定协作核心、运行时协议、动态状态层、所有文档模板、Keeper 提示词、Eval 条件、裁判提示词,以及它们的组合顺序。详见[提示词与消息目录](docs/PROMPT_CATALOG.md)。
160
163
 
161
164
  ## LOOM 刻意删除了什么
162
165
 
@@ -176,7 +179,7 @@ LOOM 2 用一个自适应理解环、可扩展的设计文档图、彼此独立
176
179
  npm test
177
180
  ```
178
181
 
179
- v2 测试套件(20 个端到端测试)覆盖完整闭环,包括 250 个 Task 的 Work Map、上下文选择、决策替代历史、可扩展设计文档、专业领域分离、能力编译(含 source 引用校验)、多轮 Keeper 修订(含 auto-pass)、陈旧 digest 与重复 run 拒绝、精确文件级 Task 启动、阻塞与重开(包括完成证据被推翻)、逐 acceptance 条件证据、交付物覆盖、决策记录与受影响 Task 警告,以及 Evil Eval 的控制变量。详见[完整 UX 与闭环规范](docs/UX_FLOW.md)。
182
+ v2 测试套件(21 个端到端测试)覆盖完整闭环,包括 250 个 Task 的 Work Map、上下文选择、决策替代历史、可扩展设计文档、专业领域分离、能力编译(含 source 引用校验和人类/Agent 权限来源)、Task 设计与能力关联、声明产物存在性、多轮 Keeper 修订(含独立审查来源与 auto-pass)、陈旧 digest 与重复 run 拒绝、精确文件级 Task 启动、阻塞与重开(包括完成证据被推翻)、逐 acceptance 条件证据、交付物覆盖、决策记录与受影响 Task 警告,以及 Evil Eval 的控制变量。详见[完整 UX 与闭环规范](docs/UX_FLOW.md)。
180
183
 
181
184
  ## 文档
182
185
 
package/cli/bin/loom.js CHANGED
@@ -67,7 +67,7 @@ function jsonFile() {
67
67
  }
68
68
  }
69
69
 
70
- function help() {
70
+ function help() {
71
71
  return `loom — continuity infrastructure for Agents
72
72
 
73
73
  The human talks naturally to the Agent. The Agent uses these commands silently.
@@ -105,7 +105,8 @@ Preserve understanding
105
105
  loom capability list|get <slug>
106
106
  loom capability research <slug> --field <text> (creates research/_guide.md — add .md files there)
107
107
  loom capability synthesize <slug> (builds decision tree from research, validates sources)
108
- loom capability confirm <slug> --scenario <text> (user confirms which expert scenario applies)
108
+ loom capability confirm <slug> --scenario <text> --source human|agent
109
+ — human confirms, or Agent records a provisional selection
109
110
  loom capability status <slug>
110
111
 
111
112
  Map the delivery surface
@@ -137,16 +138,73 @@ Evaluate LOOM itself
137
138
  Use \`--state-dir <outside-workspace-dir>\` on every command to keep LOOM state in an isolated sidecar
138
139
  (for example, a benchmark runner's per-run state directory). Sidecar initialization never edits AGENTS.md.
139
140
 
140
- Use JSON files for structured writes so long content and shell quoting remain auditable.
141
- Task completion JSON includes evidence plus either acceptance_results[] (one per acceptance criterion,
142
- each with concrete evidence) or checks[] (one per done_when criterion, for legacy tasks).`;
143
- }
141
+ Use \`loom <command> --help\` or \`loom help <topic>\` for canonical JSON payloads. Topics:
142
+ record, decision, task-plan, task-update, task-block, task-done, keeper-record, eval-scaffold
143
+
144
+ Use JSON files for structured writes so long content and shell quoting remain auditable.
145
+ Task completion JSON includes evidence plus either acceptance_results[] (one per acceptance criterion,
146
+ each with concrete evidence) or checks[] (one per done_when criterion, for legacy tasks).`;
147
+ }
148
+
149
+ const STRUCTURED_HELP = {
150
+ record: {
151
+ usage: 'loom record --json-file <update.json>',
152
+ example: {
153
+ confirmed: ['A fact confirmed by the human or workspace.'],
154
+ assumptions: [{ text: 'A bounded, reversible Agent assumption.', source: 'agent' }],
155
+ unresolved: [{ question: 'A consequential question still open?', impact: 'high' }],
156
+ decisions: [{ title: 'Decision title', decision: 'Current decision.', rationale: 'Why it follows.', supersedes: [], affects: ['.loom/PROJECT.md'] }],
157
+ },
158
+ },
159
+ decision: {
160
+ usage: 'loom decision --json-file <decision.json>',
161
+ example: { summary: 'What changed and why.', changes: ['.loom/design/system.md', 'src/system.js'], affected_tasks: ['TASK-001'] },
162
+ },
163
+ 'task-plan': {
164
+ usage: 'loom task plan --json-file <tasks.json>',
165
+ example: { tasks: [{ title: 'Create one verifiable result', outcome: 'A concrete artifact behaves as specified.', acceptance: [{ criterion: 'Observable condition', verify_by: 'Exact command or review method', evidence: '' }], boundaries: ['Does not change unrelated behavior'], depends_on: [], reads: ['.loom/PROJECT.md', '.loom/design/system.md'], touches: ['src/result.js'], implements: '.loom/design/system.md#Decision', capability_hooks: [{ node: 'field#C1', at: 'decision point', must_produce: 'project-specific choice' }], covers: ['DLV-001'] }] },
166
+ },
167
+ 'task-update': {
168
+ usage: 'loom task update <id> --json-file <patch.json>',
169
+ example: { progress: { completed: ['Finished checkpoint'], current: 'Verifying behavior', next: 'Run the named acceptance check' } },
170
+ },
171
+ 'task-block': {
172
+ usage: 'loom task block <id> --json-file <block.json>',
173
+ example: { reason: 'A concrete dependency or authority is unavailable.', recovery_conditions: ['Observable condition that permits resuming'], evidence: ['Inspection or command output showing the block'] },
174
+ },
175
+ 'task-done': {
176
+ usage: 'loom task done <id> --json-file <evidence.json>',
177
+ example: { evidence: ['Overall reproducible verification result'], acceptance_results: [{ criterion: 'Exact acceptance criterion from the Task', evidence: 'Concrete command, artifact, or observation' }] },
178
+ },
179
+ 'keeper-record': {
180
+ usage: 'loom keeper record --json-file <result.json>',
181
+ example: { run_id: 'fresh-agent-run-001', prepared_digest: '<digest from loom project ready>', verdict: 'passed', review: { mode: 'independent', reviewer_id: 'fresh-agent-001', evidence: 'Host opened a separate Agent without the shaping conversation.' }, summary: 'Build-readiness judgment.', gaps: [], evidence: ['Files and observations supporting the verdict'] },
182
+ },
183
+ 'eval-scaffold': {
184
+ usage: 'loom eval scaffold --json-file <scenario.json>',
185
+ example: { id: 'EVAL-001', title: 'Ambiguous real project', brief: 'Identical brief for both conditions.', hidden_user_facts: ['Fact revealed by the same answer script'], human_channel: 'unavailable', success_criteria: ['Observable result'], context_reset_points: ['after-shaping', 'mid-task'], repetitions: 3 },
186
+ },
187
+ };
188
+
189
+ function structuredHelp(topic) {
190
+ const entry = STRUCTURED_HELP[topic];
191
+ if (!entry) throw new Error(`Unknown help topic: ${topic}. Available topics: ${Object.keys(STRUCTURED_HELP).join(', ')}`);
192
+ return `${entry.usage}\n\nCanonical JSON payload:\n${JSON.stringify(entry.example, null, 2)}`;
193
+ }
194
+
195
+ function activeStructuredHelpTopic() {
196
+ if (command === 'record' || command === 'decision') return command;
197
+ if (command === 'task' && ['plan', 'update', 'block', 'done'].includes(subcommand)) return `task-${subcommand}`;
198
+ if (command === 'keeper' && subcommand === 'record') return 'keeper-record';
199
+ if (command === 'eval' && subcommand === 'scaffold') return 'eval-scaffold';
200
+ return '';
201
+ }
144
202
 
145
203
  try {
146
204
  configureRuntime({ stateDir: option('--state-dir') });
147
205
  const humanChannel = option('--human-channel');
148
206
  if (humanChannel && !['available', 'unavailable'].includes(humanChannel)) throw new Error('--human-channel must be available or unavailable');
149
- switch (command) {
207
+ switch (command) {
150
208
  case '--version':
151
209
  case '-v': {
152
210
  const here = dirname(fileURLToPath(import.meta.url));
@@ -154,11 +212,14 @@ try {
154
212
  output(`loom ${pkg.version}`);
155
213
  break;
156
214
  }
157
- case '--help':
215
+ case '--help':
158
216
  case '-h':
159
217
  case undefined:
160
- output(help());
161
- break;
218
+ output(help());
219
+ break;
220
+ case 'help':
221
+ output(subcommand ? structuredHelp(subcommand) : help());
222
+ break;
162
223
  case 'init':
163
224
  output(initProject());
164
225
  break;
@@ -169,11 +230,11 @@ try {
169
230
  case 'prompts':
170
231
  output(promptCatalog());
171
232
  break;
172
- case 'record':
173
- output(recordUnderstanding(jsonFile()));
174
- break;
175
- case 'decision':
176
- output(recordDecision(jsonFile()));
233
+ case 'record':
234
+ output(subcommand === '--help' ? structuredHelp('record') : recordUnderstanding(jsonFile()));
235
+ break;
236
+ case 'decision':
237
+ output(subcommand === '--help' ? structuredHelp('decision') : recordDecision(jsonFile()));
177
238
  break;
178
239
  case 'check': {
179
240
  const result = checkProject();
@@ -217,7 +278,7 @@ try {
217
278
  output(synthesizeCapability(rest[0]));
218
279
  } else if (subcommand === 'confirm') {
219
280
  if (!rest[0]) throw new Error('Usage: loom capability confirm <slug> --scenario <text>');
220
- output(confirmCapability(rest[0], { scenario: option('--scenario') }));
281
+ output(confirmCapability(rest[0], { scenario: option('--scenario'), source: option('--source') }));
221
282
  } else if (subcommand === 'status') {
222
283
  if (!rest[0]) throw new Error('Usage: loom capability status <slug>');
223
284
  output(getCapabilityStatus(rest[0]));
@@ -236,8 +297,9 @@ try {
236
297
  else throw new Error('Usage: loom deliverable add|list|coverage');
237
298
  break;
238
299
  }
239
- case 'task': {
240
- if (subcommand === 'plan') output(importTasks(jsonFile()));
300
+ case 'task': {
301
+ if (argv.includes('--help') && ['plan', 'update', 'block', 'done'].includes(subcommand)) output(structuredHelp(`task-${subcommand}`));
302
+ else if (subcommand === 'plan') output(importTasks(jsonFile()));
241
303
  else if (subcommand === 'status') output(taskSummary(loadProject().taskStore.tasks));
242
304
  else if (subcommand === 'next') output(getTask());
243
305
  else if (subcommand === 'get') {
@@ -261,20 +323,26 @@ try {
261
323
  } else throw new Error('Usage: loom task plan|status|next|get|start|update|block|reopen|done');
262
324
  break;
263
325
  }
264
- case 'keeper': {
265
- if (subcommand === 'prompt') output(getKeeperPrompt());
326
+ case 'keeper': {
327
+ if (argv.includes('--help') && subcommand === 'record') output(structuredHelp('keeper-record'));
328
+ else if (subcommand === 'prompt') output(getKeeperPrompt());
266
329
  else if (subcommand === 'record') output(recordKeeper(jsonFile()));
267
330
  else if (subcommand === 'skip') output(skipKeeper(option('--reason')));
268
331
  else throw new Error('Usage: loom keeper prompt|record|skip');
269
332
  break;
270
333
  }
271
- case 'eval':
272
- if (subcommand !== 'scaffold') throw new Error('Usage: loom eval scaffold --json-file <scenario.json>');
273
- output(scaffoldEval(jsonFile()));
334
+ case 'eval':
335
+ if (argv.includes('--help') && subcommand === 'scaffold') output(structuredHelp('eval-scaffold'));
336
+ else {
337
+ if (subcommand !== 'scaffold') throw new Error('Usage: loom eval scaffold --json-file <scenario.json>');
338
+ output(scaffoldEval(jsonFile()));
339
+ }
274
340
  break;
275
341
  default:
276
342
  throw new Error(`Unknown command: ${command}\n\n${help()}`);
277
343
  }
278
- } catch (error) {
279
- fail(error.message);
280
- }
344
+ } catch (error) {
345
+ const topic = activeStructuredHelpTopic();
346
+ const pointer = topic && !argv.includes('--help') ? `\nRun ${STRUCTURED_HELP[topic].usage.replace(/ --json-file .+$/, ' --help').replace(/ <id>/, '')} for a canonical payload.` : '';
347
+ fail(`${error.message}${pointer}`);
348
+ }
@@ -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), confirm (the user
80
- confirms which expert scenario this project most resembles — the Agent must not decide this alone),
81
- and confirmed. \`loom capability research\` creates a \`_guide.md\` in the research directory explaining
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, and before substantial work. When a Task is active, the command restores
226
- its execution protocol, exact state, and declared context; rerun it before editing after any interruption.
227
- Keep project truth, design documents, professional capability dossiers, and Tasks current through the CLI,
228
- and never ask the human to operate LOOM. Do not rerun LOOM before every tool call: persist and restore at
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
- "summary": "<concise handoff judgment>",
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. Otherwise use \`needs_revision\` with concrete gaps and the
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 confirms the scenario).
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 the user-confirmed project scenario: which expert situation this project most
525
- > closely resembles. The Agent must not fill this alone; it requires user confirmation via
526
- > \`loom capability confirm <slug> --scenario <text>\`.
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 !== 'synthesized') throw new Error(`Capability ${slug} must be synthesized before confirmation. Run loom capability synthesize ${slug} first.`);
416
- status.scenario = options.scenario;
417
- status.status = 'confirmed';
418
- status.confirmed_at = now();
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: 'confirmed', scenario: options.scenario };
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
- capability_hooks: raw.capability_hooks || [],
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
- export function updateTask(id, patch, root = findRoot()) {
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 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: ${capabilities.length}\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.`;
838
- const blocks = [statusBlock, agentProtocol({ humanChannel: options.humanChannel || 'available' }), shapingContext({ state, taskSummary: summary, capabilityNames: capabilities, designNames: designs, forKeeper: Boolean(options.keeper) })];
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 errors = [];
895
- const warnings = [];
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 (!listCapabilities(root).length) warnings.push('No capability dossier exists; acceptable only when specialist judgment would not change the work');
923
- if (!listDesigns(root).length) warnings.push('No design document exists; PROJECT.md should remain a concise map of the whole');
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 listDesigns(root)) {
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 listCapabilities(root)) {
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`. It may return gaps; summary, evidence and exact gaps
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
 
@@ -157,11 +169,12 @@ 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 criterion-by-criterion evidence.
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
 
@@ -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
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@haaaiawd/loom",
3
- "version": "2.1.1",
4
- "description": "LOOM — invisible project understanding and continuity infrastructure for AI agents",
3
+ "version": "2.1.2",
4
+ "description": "LOOM — Spec-driven continuity, proof, and project understanding infrastructure for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "loom": "cli/bin/loom.js"
@@ -33,11 +33,15 @@
33
33
  },
34
34
  "keywords": [
35
35
  "loom",
36
- "agent",
36
+ "spec-driven",
37
+ "ai-agent",
38
+ "ai-coding",
37
39
  "continuity",
38
40
  "project-understanding",
39
- "task-context",
40
- "cli"
41
+ "agent-framework",
42
+ "context-management",
43
+ "task-tracking",
44
+ "llm"
41
45
  ],
42
46
  "license": "MIT",
43
47
  "repository": {