opennori 0.1.5 → 0.1.7
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/.opennori/protocol.md +25 -14
- package/README.md +9 -0
- package/examples/opennori-self.json +3 -3
- package/package.json +1 -1
- package/src/cli.js +290 -7
package/.opennori/protocol.md
CHANGED
|
@@ -145,7 +145,7 @@ OpenNori exposes a Skill Pack for agent use. The user should not need to remembe
|
|
|
145
145
|
the root `nori` Skill routes natural-language requests to focused Skills:
|
|
146
146
|
|
|
147
147
|
- `nori`: root router for OpenNori turns
|
|
148
|
-
- `nori-acceptance`: brainstorm, draft, approve, and revise human-facing ACs
|
|
148
|
+
- `nori-acceptance`: discover AC gaps, brainstorm, draft, approve, and revise human-facing ACs
|
|
149
149
|
- `nori-evidence`: record reviewable evidence without forcing fixed adapters
|
|
150
150
|
- `nori-capability-profile`: record required Skills, preferred stacks, avoided tools, and install policy
|
|
151
151
|
- `nori-project-health`: install, uninstall, doctor, manifest, and Skill Pack sync
|
|
@@ -154,6 +154,11 @@ the root `nori` Skill routes natural-language requests to focused Skills:
|
|
|
154
154
|
`opennori install --skill` installs the pack under `.agents/skills/`. The manifest records `skill_pack`
|
|
155
155
|
state, and `opennori doctor` checks whether the pack is installed and in sync.
|
|
156
156
|
|
|
157
|
+
When upgrading an existing OpenNori project, upgrade entry assets first, then run `opennori check`.
|
|
158
|
+
`check` validates active Nori Contracts and reports `acceptance_quality` warnings for vague ACs
|
|
159
|
+
such as "modify profile fields" or "show an error". It does not rewrite existing contracts, evidence,
|
|
160
|
+
reports, archives, or brainstorms. The user decides whether to revise affected criteria.
|
|
161
|
+
|
|
157
162
|
## Nori Profile
|
|
158
163
|
|
|
159
164
|
Acceptance criteria remain human-facing outcomes. A Nori Profile is separate execution
|
|
@@ -239,23 +244,27 @@ all evidence through a narrow adapter taxonomy.
|
|
|
239
244
|
|
|
240
245
|
On every turn:
|
|
241
246
|
|
|
242
|
-
1. If the user
|
|
243
|
-
2.
|
|
244
|
-
3. If the user
|
|
245
|
-
4.
|
|
246
|
-
5.
|
|
247
|
-
6.
|
|
248
|
-
7.
|
|
249
|
-
8.
|
|
250
|
-
9.
|
|
251
|
-
10.
|
|
252
|
-
11.
|
|
253
|
-
12. Run `opennori
|
|
254
|
-
13.
|
|
247
|
+
1. If the user gives a fuzzy goal or candidate AC, run `opennori discover --goal "<goal>" --root <repo> --json` before drafting.
|
|
248
|
+
2. Ask only the discovery questions that affect completion judgment. Do not turn discovery gaps into implementation tasks or completion evidence.
|
|
249
|
+
3. If the user wants to discuss, brainstorm, explore, or is not ready to define acceptance criteria, run `opennori brainstorm --idea "<idea>" --root <repo> --json`.
|
|
250
|
+
4. Show only candidate acceptance directions and ask the user to choose or revise a direction. Brainstorm output is not a contract or completion evidence.
|
|
251
|
+
5. If the user chooses a candidate, run `opennori draft --from-brainstorm <brainstorm-id> --candidate <A|B|C> --root <repo> --json`.
|
|
252
|
+
6. If the user starts with "use OpenNori" / "用 OpenNori 跑这个任务" and discovery gaps are answered or explicitly accepted as assumptions, run `opennori draft --goal "<goal>" --root <repo> --json`.
|
|
253
|
+
7. Show the draft acceptance criteria and ask the user to approve or revise them.
|
|
254
|
+
8. After approval, run `opennori approve --root <repo> --summary "<approval>" --json`.
|
|
255
|
+
9. If the user states required Skills, preferred stacks, avoided tools, install policy, or execution constraints, run `opennori profile add --root <repo> ... --json` and keep those items out of the user acceptance criteria.
|
|
256
|
+
10. If the user revises a criterion later, run `opennori criterion update --root <repo> --criterion <id> ... --json`; old evidence for the changed criterion is cleared.
|
|
257
|
+
11. If the user asks to upgrade an existing OpenNori project, run `opennori doctor`, preview and confirm `opennori upgrade`, then run `opennori check`; ask the user before revising any existing AC flagged by `acceptance_quality`.
|
|
258
|
+
12. Run `opennori resume --root <repo>` or `opennori next --root <repo>` to recover the active goal and current acceptance gap from repository files.
|
|
259
|
+
13. Work only to produce evidence for that gap.
|
|
260
|
+
14. Add acceptance evidence with `opennori evidence add`; choose any suitable verification method, but record basis, sources, reviewability, confidence, and limitations. Add profile compliance evidence with `opennori profile evidence` when profile items exist.
|
|
261
|
+
15. Run `opennori evaluate`.
|
|
262
|
+
16. Report acceptance state, profile compliance, and evidence, not implementation steps.
|
|
255
263
|
|
|
256
264
|
Useful commands:
|
|
257
265
|
|
|
258
266
|
- `opennori brainstorm --idea "<idea>" --root <repo>`: create selectable acceptance directions before a contract exists.
|
|
267
|
+
- `opennori discover --goal "<goal>" --root <repo>`: find underspecified acceptance gaps before drafting a contract.
|
|
259
268
|
- `opennori draft --goal "<goal>" --root <repo>`: create a draft Nori Contract that needs user approval.
|
|
260
269
|
- `opennori draft --from-brainstorm <brainstorm-id> --candidate <A|B|C> --root <repo>`: convert a selected brainstorm direction into a draft contract.
|
|
261
270
|
- `opennori approve --root <repo>`: mark the acceptance basis as approved so completion can be decided.
|
|
@@ -266,7 +275,9 @@ Useful commands:
|
|
|
266
275
|
- `opennori profile show --root <repo>`: show profile compliance and blocking items.
|
|
267
276
|
- `opennori list --root <repo>`: list active OpenNori goals.
|
|
268
277
|
- `opennori install --root <repo>`: create or refresh project-local OpenNori assets and manifest.
|
|
278
|
+
- `opennori upgrade --root <repo>`: preview and refresh project-local OpenNori assets without rewriting active contracts or evidence.
|
|
269
279
|
- `opennori doctor --root <repo>`: inspect project OpenNori health and recovery actions.
|
|
280
|
+
- `opennori check --root <repo>`: validate active contract structure and audit active ACs for underspecified acceptance quality.
|
|
270
281
|
- `opennori resume --root <repo>`: recover the active goal, current gap, completion answer, and intervention state.
|
|
271
282
|
- `opennori status --root <repo>`: answer whether the goal is complete and whether the user needs to act.
|
|
272
283
|
- `opennori report --root <repo>`: generate the human acceptance report.
|
package/README.md
CHANGED
|
@@ -45,6 +45,10 @@ Use OpenNori for this project. Start from my goal, define a Nori Contract,
|
|
|
45
45
|
and keep working only from acceptance gaps until the report says whether it is complete.
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
For fuzzy goals, ask Nori to discover the real acceptance gaps first. It should ask about missing
|
|
49
|
+
field scope, validation rules, success signals, persistence, failure cases, and out-of-scope
|
|
50
|
+
boundaries before it turns the goal into a Nori Contract.
|
|
51
|
+
|
|
48
52
|
## What Gets Added
|
|
49
53
|
|
|
50
54
|
OpenNori uses one project-local state directory:
|
|
@@ -69,6 +73,7 @@ It does not create a `process/` directory as the main workflow surface.
|
|
|
69
73
|
```bash
|
|
70
74
|
opennori bootstrap
|
|
71
75
|
opennori doctor --root .
|
|
76
|
+
opennori discover --goal "Ship a settings page" --root .
|
|
72
77
|
opennori brainstorm --idea "Explore this goal" --root .
|
|
73
78
|
opennori draft --goal "Ship a user-visible result" --root .
|
|
74
79
|
opennori approve --root . --summary "User approved the acceptance checks."
|
|
@@ -87,6 +92,10 @@ language requests to the deterministic CLI state layer.
|
|
|
87
92
|
require explicit confirmation.
|
|
88
93
|
- In a human terminal, `npx opennori` is interactive. With `--json` or non-interactive stdio it
|
|
89
94
|
returns structured JSON for agents and automation.
|
|
95
|
+
- `discover` finds underspecified acceptance gaps before draft, so vague ACs such as "modify fields"
|
|
96
|
+
or "show an error" become user questions instead of weak contracts.
|
|
97
|
+
- Existing projects keep their Nori Contracts and evidence during upgrade. After upgrading, `check`
|
|
98
|
+
audits active contracts for vague ACs and asks the user before any revision.
|
|
90
99
|
- `doctor` reports whether project state is `ready`, `needs-action`, or `broken`, with recovery
|
|
91
100
|
actions.
|
|
92
101
|
- Nori Profile records required Skills, preferred stacks, avoided tools, and install policy without
|
|
@@ -97,9 +97,9 @@
|
|
|
97
97
|
{
|
|
98
98
|
"id": "AC-P-12",
|
|
99
99
|
"layer": "protocol",
|
|
100
|
-
"user_story": "作为用户,我运行 opennori check
|
|
101
|
-
"measurement": "
|
|
102
|
-
"threshold": "
|
|
100
|
+
"user_story": "作为用户,我运行 opennori check 或让 agent 升级已使用 OpenNori 的项目后,能发现现有 active contract 里过于含糊的 AC,而不是让旧的弱验收标准静默通过。",
|
|
101
|
+
"measurement": "对已有 active goal 中包含“修改字段”“失败时有提示”等空泛完成条件的 Nori Contract 运行 opennori check;再对结构正确且包含具体用户操作、字段范围、反馈和判断方式的 contract 运行 check。",
|
|
102
|
+
"threshold": "check 在不改写历史 contract/evidence 的前提下输出 acceptance_quality 审计结果、warnings 和下一步建议;弱 AC 会标出缺少的字段范围、校验规则、成功反馈、失败场景或范围边界;具体 AC 不产生质量告警。",
|
|
103
103
|
"risk": "high"
|
|
104
104
|
},
|
|
105
105
|
{
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -38,6 +38,8 @@ const NORI_CAPABILITIES = [
|
|
|
38
38
|
"reviewable-evidence",
|
|
39
39
|
"skill-pack",
|
|
40
40
|
"brainstorm",
|
|
41
|
+
"acceptance-discovery",
|
|
42
|
+
"acceptance-quality-audit",
|
|
41
43
|
"capability-profile",
|
|
42
44
|
"profile-check",
|
|
43
45
|
"archive",
|
|
@@ -371,6 +373,191 @@ const DEFAULT_CRITERIA = [
|
|
|
371
373
|
}
|
|
372
374
|
];
|
|
373
375
|
|
|
376
|
+
const DISCOVERY_GAPS = [
|
|
377
|
+
{
|
|
378
|
+
id: "missing-field-scope",
|
|
379
|
+
patterns: ["设置", "资料", "个人资料", "profile", "settings", "字段", "field"],
|
|
380
|
+
question: "本轮用户可以修改或查看哪些具体字段?哪些字段明确不在范围内?",
|
|
381
|
+
why: "没有字段范围,用户无法判断修改能力是否完整。"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: "missing-validation-rule",
|
|
385
|
+
patterns: ["修改", "输入", "保存", "上传", "表单", "edit", "input", "save", "upload", "form"],
|
|
386
|
+
question: "每个可输入内容的有效规则是什么,例如长度、必填、格式、文件类型或大小?",
|
|
387
|
+
why: "没有校验规则,失败和边界输入无法验收。"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: "missing-success-signal",
|
|
391
|
+
patterns: ["保存", "提交", "创建", "更新", "完成", "save", "submit", "create", "update"],
|
|
392
|
+
question: "操作成功后,用户会看到什么明确反馈或结果变化?",
|
|
393
|
+
why: "没有成功反馈,用户无法判断操作是否真的完成。"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: "missing-persistence-scope",
|
|
397
|
+
patterns: ["保存", "刷新", "重新打开", "重新登录", "持久", "save", "refresh", "reload", "reopen", "login", "persist"],
|
|
398
|
+
question: "结果需要在刷新、重新打开、重新登录或跨设备后仍然存在吗?",
|
|
399
|
+
why: "没有持久化范围,完成判断会在当前页面和真实保存之间摇摆。"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "missing-failure-case",
|
|
403
|
+
patterns: ["失败", "错误", "提示", "网络", "权限", "error", "fail", "failure", "invalid", "permission", "network"],
|
|
404
|
+
question: "哪些失败情况必须覆盖,用户分别应该看到什么提示或保留什么原状态?",
|
|
405
|
+
why: "没有失败场景,错误体验可能被一句“有提示”掩盖。"
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
id: "missing-out-of-scope-boundary",
|
|
409
|
+
patterns: ["页面", "页", "设置", "功能", "支持", "完成", "page", "feature", "support", "complete"],
|
|
410
|
+
question: "哪些相关能力明确不属于本轮完成范围?",
|
|
411
|
+
why: "没有范围边界,agent 可能扩大实现,也可能漏掉用户真正期待的部分。"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "missing-user-entry",
|
|
415
|
+
patterns: ["使用", "打开", "查看", "进入", "run", "open", "view", "use", "entry"],
|
|
416
|
+
question: "用户从哪个入口开始操作,最终在哪里查看结果?",
|
|
417
|
+
why: "没有用户入口,AC 容易变成内部状态而不是可执行验收。"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: "missing-review-method",
|
|
421
|
+
patterns: ["判断", "验收", "完成", "review", "accept", "done", "complete"],
|
|
422
|
+
question: "用户或评审者应该用什么可复查方式判断这条 AC 通过?",
|
|
423
|
+
why: "没有复查方式,完成判断会退化成 agent 自我总结。"
|
|
424
|
+
}
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
function sentenceHasSpecifics(text, terms) {
|
|
428
|
+
const value = String(text || "").toLowerCase();
|
|
429
|
+
return terms.some((term) => value.includes(term.toLowerCase()));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function discoverAcceptanceGaps(text, { fallback = false, allowedIds = null } = {}) {
|
|
433
|
+
const lowered = text.toLowerCase();
|
|
434
|
+
const gaps = DISCOVERY_GAPS
|
|
435
|
+
.filter((gap) => !allowedIds || allowedIds.has(gap.id))
|
|
436
|
+
.filter((gap) => gap.patterns.some((pattern) => lowered.includes(pattern.toLowerCase())))
|
|
437
|
+
.filter((gap) => {
|
|
438
|
+
if (gap.id === "missing-field-scope") return !sentenceHasSpecifics(text, ["昵称", "头像", "简介", "邮箱", "手机号", "字段范围", "field scope", "name", "avatar", "bio", "email", "phone"]);
|
|
439
|
+
if (gap.id === "missing-validation-rule") return !sentenceHasSpecifics(text, ["长度", "必填", "格式", "大小", "类型", "字符", "校验规则", "validation", "required", "format", "length", "size", "type"]);
|
|
440
|
+
if (gap.id === "missing-success-signal") return !sentenceHasSpecifics(text, ["成功", "保存成功", "成功反馈", "result", "success"]);
|
|
441
|
+
if (gap.id === "missing-persistence-scope") return !sentenceHasSpecifics(text, ["刷新", "重新打开", "重新登录", "跨设备", "refresh", "reload", "reopen", "login"]);
|
|
442
|
+
if (gap.id === "missing-failure-case") return !sentenceHasSpecifics(text, ["网络", "权限", "无效", "错误码", "保留原", "失败场景", "network", "permission", "invalid"]);
|
|
443
|
+
if (gap.id === "missing-out-of-scope-boundary") return !sentenceHasSpecifics(text, ["不在范围", "不包含", "本轮不", "范围边界", "out of scope", "exclude"]);
|
|
444
|
+
if (gap.id === "missing-user-entry") return !sentenceHasSpecifics(text, ["设置页", "登录页", "report", "dashboard", "页面", "page"]);
|
|
445
|
+
if (gap.id === "missing-review-method") return !sentenceHasSpecifics(text, ["截图", "浏览器", "报告", "测试", "review", "screenshot", "browser", "report"]);
|
|
446
|
+
return true;
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (gaps.length > 0 || !fallback) return gaps;
|
|
450
|
+
return [
|
|
451
|
+
{
|
|
452
|
+
id: "missing-review-method",
|
|
453
|
+
question: "用户或评审者应该用什么可复查方式判断这个目标完成?",
|
|
454
|
+
why: "OpenNori 需要先知道完成判断方式,才能形成真正可验收的 AC。"
|
|
455
|
+
}
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function discoveryGap(gapId) {
|
|
460
|
+
return DISCOVERY_GAPS.find((gap) => gap.id === gapId);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function discoverAcceptance(goal, explicitId = undefined) {
|
|
464
|
+
const text = String(goal || "").trim();
|
|
465
|
+
const selectedGaps = discoverAcceptanceGaps(text, { fallback: true });
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
protocol_version: "opennori/discovery-v1",
|
|
469
|
+
id: explicitId || slugify(text.slice(0, 40) || "acceptance-discovery"),
|
|
470
|
+
goal: text,
|
|
471
|
+
status: selectedGaps.length > 0 ? "needs-user-answers" : "ready-for-draft",
|
|
472
|
+
is_acceptance_contract: false,
|
|
473
|
+
gaps: selectedGaps.map((gap, index) => ({
|
|
474
|
+
id: gap.id,
|
|
475
|
+
question: gap.question,
|
|
476
|
+
why: gap.why,
|
|
477
|
+
priority: index < 3 ? "must-answer" : "can-default"
|
|
478
|
+
})),
|
|
479
|
+
next: "Ask the must-answer questions before drafting a Nori Contract. Use assumptions only when the user accepts them."
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function auditAcceptanceQuality(contract) {
|
|
484
|
+
const findings = [];
|
|
485
|
+
for (const [index, criterion] of (contract.criteria || []).entries()) {
|
|
486
|
+
const triggerText = [
|
|
487
|
+
criterion.user_story,
|
|
488
|
+
criterion.threshold
|
|
489
|
+
].filter(Boolean).join("\n");
|
|
490
|
+
const fullText = [
|
|
491
|
+
criterion.user_story,
|
|
492
|
+
criterion.measurement,
|
|
493
|
+
criterion.threshold
|
|
494
|
+
].filter(Boolean).join("\n");
|
|
495
|
+
const addFinding = (gapId) => {
|
|
496
|
+
const gap = discoveryGap(gapId);
|
|
497
|
+
if (!gap) return;
|
|
498
|
+
findings.push({
|
|
499
|
+
criterion_id: criterion.id,
|
|
500
|
+
path: `criteria[${index}]`,
|
|
501
|
+
gap_id: gap.id,
|
|
502
|
+
question: gap.question,
|
|
503
|
+
why: gap.why,
|
|
504
|
+
severity: "needs-user-review"
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const vagueEditableProfile = sentenceHasSpecifics(triggerText, [
|
|
509
|
+
"修改个人资料",
|
|
510
|
+
"修改资料",
|
|
511
|
+
"修改字段",
|
|
512
|
+
"编辑个人资料",
|
|
513
|
+
"编辑资料",
|
|
514
|
+
"edit profile",
|
|
515
|
+
"update profile",
|
|
516
|
+
"modify fields",
|
|
517
|
+
"edit fields"
|
|
518
|
+
]);
|
|
519
|
+
if (vagueEditableProfile && !sentenceHasSpecifics(fullText, ["昵称", "头像", "简介", "邮箱", "手机号", "字段范围", "field scope", "name", "avatar", "bio", "email", "phone"])) {
|
|
520
|
+
addFinding("missing-field-scope");
|
|
521
|
+
}
|
|
522
|
+
if (vagueEditableProfile && !sentenceHasSpecifics(fullText, ["长度", "必填", "格式", "大小", "类型", "字符", "校验规则", "validation", "required", "format", "length", "size", "type"])) {
|
|
523
|
+
addFinding("missing-validation-rule");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const mentionsSave = sentenceHasSpecifics(triggerText, ["保存", "提交", "save", "submit"]);
|
|
527
|
+
if (mentionsSave && !sentenceHasSpecifics(fullText, ["保存成功", "成功反馈", "成功提示", "显示成功", "报告显示", "result", "success"])) {
|
|
528
|
+
addFinding("missing-success-signal");
|
|
529
|
+
}
|
|
530
|
+
if (mentionsSave && !sentenceHasSpecifics(fullText, ["刷新", "重新打开", "重新登录", "跨设备", "refresh", "reload", "reopen", "login"])) {
|
|
531
|
+
addFinding("missing-persistence-scope");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const vagueFailure = sentenceHasSpecifics(triggerText, [
|
|
535
|
+
"失败时有提示",
|
|
536
|
+
"失败提示",
|
|
537
|
+
"有提示",
|
|
538
|
+
"错误提示",
|
|
539
|
+
"show an error",
|
|
540
|
+
"error message"
|
|
541
|
+
]);
|
|
542
|
+
if (vagueFailure && !sentenceHasSpecifics(fullText, ["网络", "权限", "无效", "错误码", "保留原", "失败场景", "network", "permission", "invalid"])) {
|
|
543
|
+
addFinding("missing-failure-case");
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const broadSettingsScope = sentenceHasSpecifics(triggerText, ["设置页", "个人资料", "settings page"]);
|
|
547
|
+
if (broadSettingsScope && !sentenceHasSpecifics(fullText, ["不在范围", "不包含", "本轮不", "范围边界", "out of scope", "exclude"])) {
|
|
548
|
+
addFinding("missing-out-of-scope-boundary");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return {
|
|
553
|
+
status: findings.length > 0 ? "needs-user-review" : "clear",
|
|
554
|
+
summary: findings.length > 0
|
|
555
|
+
? `${findings.length} acceptance quality gap(s) may need user review.`
|
|
556
|
+
: "No underspecified acceptance gaps found.",
|
|
557
|
+
findings
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
374
561
|
function printJson(payload) {
|
|
375
562
|
console.log(JSON.stringify(payload, null, 2));
|
|
376
563
|
}
|
|
@@ -398,7 +585,7 @@ function isInteractive(args) {
|
|
|
398
585
|
}
|
|
399
586
|
|
|
400
587
|
const CLI_NAME = "opennori";
|
|
401
|
-
const TOP_LEVEL_USAGE = `${CLI_NAME} <bootstrap|doctor|install|upgrade|uninstall|brainstorm|draft|init|list|check|approve|criterion|profile|resume|next|evidence|evaluate|status|report|context|changes|archive|skill>`;
|
|
588
|
+
const TOP_LEVEL_USAGE = `${CLI_NAME} <bootstrap|doctor|install|upgrade|uninstall|brainstorm|discover|draft|init|list|check|approve|criterion|profile|resume|next|evidence|evaluate|status|report|context|changes|archive|skill>`;
|
|
402
589
|
|
|
403
590
|
function wantsHelp(args) {
|
|
404
591
|
return args.includes("--help") || args.includes("-h");
|
|
@@ -413,6 +600,7 @@ function usageFor(args) {
|
|
|
413
600
|
if (command === "uninstall") return `${CLI_NAME} uninstall --root <project> [--include-state] [--dry-run] [--confirm] [--json]`;
|
|
414
601
|
if (command === "doctor") return `${CLI_NAME} doctor --root <project> [--json]`;
|
|
415
602
|
if (command === "brainstorm") return `${CLI_NAME} brainstorm --idea "<idea>" --root <project> [--id <id>] [--json]`;
|
|
603
|
+
if (command === "discover") return `${CLI_NAME} discover --goal "<goal>" --root <project> [--id <id>] [--json]`;
|
|
416
604
|
if (command === "draft") return `${CLI_NAME} draft --goal "<goal>" --root <project> [--goal-id <id>] [--json]`;
|
|
417
605
|
if (command === "init") return `${CLI_NAME} init <brief.json> --root <project> [--json]`;
|
|
418
606
|
if (command === "criterion" && subcommand === "update") return `${CLI_NAME} criterion update --root <project> --criterion <id> --user-story ... --measurement ... --threshold ... [--json]`;
|
|
@@ -573,7 +761,7 @@ const SKILL_PACK = [
|
|
|
573
761
|
"Use when the user mentions OpenNori, asks to use OpenNori for a task, continue OpenNori, check completion, inspect project health, define acceptance criteria, record evidence, manage capability preferences, or produce an OpenNori report.",
|
|
574
762
|
"",
|
|
575
763
|
"## Route",
|
|
576
|
-
"- Goal, brainstorm, approval, or AC revision -> use `nori-acceptance`.",
|
|
764
|
+
"- Goal, acceptance discovery, brainstorm, approval, or AC revision -> use `nori-acceptance`.",
|
|
577
765
|
"- Verification, evidence sufficiency, human confirmation, waiver, or why an AC is passing -> use `nori-evidence`.",
|
|
578
766
|
"- Required Skills, preferred stacks, avoided tools, or install policy -> use `nori-capability-profile`.",
|
|
579
767
|
"- Install, uninstall, doctor, manifest, Skill sync, or project recoverability -> use `nori-project-health`.",
|
|
@@ -596,9 +784,10 @@ const SKILL_PACK = [
|
|
|
596
784
|
description: "Create, review, approve, and revise OpenNori human-centered acceptance criteria from natural language goals.",
|
|
597
785
|
body: [
|
|
598
786
|
"## When to use",
|
|
599
|
-
"Use when the user gives a goal, wants to brainstorm acceptance directions, approves criteria, revises completion criteria, or says the AC is wrong.",
|
|
787
|
+
"Use when the user gives a goal, wants to discover real acceptance criteria, wants to brainstorm acceptance directions, approves criteria, revises completion criteria, or says the AC is wrong.",
|
|
600
788
|
"",
|
|
601
789
|
"## Commands",
|
|
790
|
+
"- Before drafting from a fuzzy goal: `opennori discover --goal \"<goal>\" --root <repo> --json`.",
|
|
602
791
|
"- Fuzzy idea or discussion: `opennori brainstorm --idea \"<idea>\" --root <repo> --json`.",
|
|
603
792
|
"- Start from a goal: `opennori draft --goal \"<goal>\" --root <repo> --json`.",
|
|
604
793
|
"- Start from a chosen brainstorm candidate: `opennori draft --from-brainstorm <brainstorm-id> --candidate <A|B|C> --root <repo> --json`.",
|
|
@@ -606,6 +795,9 @@ const SKILL_PACK = [
|
|
|
606
795
|
"- User revises a criterion: `opennori criterion update --root <repo> --criterion <id> --user-story ... --measurement ... --threshold ... --json`.",
|
|
607
796
|
"",
|
|
608
797
|
"## Rules",
|
|
798
|
+
"Run discovery before draft when the goal or candidate AC contains vague verbs such as modify, save, support, show an error, or improve.",
|
|
799
|
+
"Discovery gaps are questions for the user, not implementation tasks and not completion evidence.",
|
|
800
|
+
"Do not draft generic ACs like 'modify fields' or 'show failure prompt' until field scope, validation rules, success signal, persistence scope, failure cases, and out-of-scope boundaries are clear enough for the user to judge.",
|
|
609
801
|
"ACs must describe user actions or judgments, not implementation files, commands, modules, fields, tests, Skills, or technology choices.",
|
|
610
802
|
"Capability preferences belong in the Nori Profile, not user ACs.",
|
|
611
803
|
"Do not treat brainstorm output as a Nori Contract or completion evidence."
|
|
@@ -660,16 +852,20 @@ const SKILL_PACK = [
|
|
|
660
852
|
"- Confirm first-time setup after user approval: `opennori bootstrap --root <repo> --confirm --json`.",
|
|
661
853
|
"- Preview install: `opennori install --root <repo> --dry-run --json`.",
|
|
662
854
|
"- Install Skill Pack: `opennori install --root <repo> --skill --json`.",
|
|
855
|
+
"- Preview upgrade: `opennori upgrade --root <repo> --skill --dry-run --json`.",
|
|
856
|
+
"- Confirm upgrade after user approval: `opennori upgrade --root <repo> --skill --confirm --json`.",
|
|
663
857
|
"- Preview destructive install: `opennori install --root <repo> --skill --force --dry-run --json`.",
|
|
664
858
|
"- Confirm destructive install: `opennori install --root <repo> --skill --force --confirm --json`.",
|
|
665
859
|
"- Doctor: `opennori doctor --root <repo> --json`.",
|
|
860
|
+
"- Existing contract check after upgrade: `opennori check --root <repo> --json`.",
|
|
666
861
|
"- Preview uninstall: `opennori uninstall --root <repo> --dry-run --json`.",
|
|
667
862
|
"- Remove entry assets while preserving state: `opennori uninstall --root <repo> --confirm --json`.",
|
|
668
863
|
"- Remove all OpenNori state only after explicit user acceptance: `opennori uninstall --root <repo> --include-state --confirm --json`.",
|
|
669
864
|
"",
|
|
670
865
|
"## Rules",
|
|
671
866
|
"Always show dry-run plans before destructive writes.",
|
|
672
|
-
"Default uninstall preserves active goals, evidence, reports, archives, and brainstorms."
|
|
867
|
+
"Default uninstall preserves active goals, evidence, reports, archives, and brainstorms.",
|
|
868
|
+
"Upgrade must preserve existing active contracts and evidence. After upgrade, run `opennori check` and route any `acceptance_quality` warnings to `nori-acceptance` for user-approved revision."
|
|
673
869
|
]
|
|
674
870
|
},
|
|
675
871
|
{
|
|
@@ -1354,6 +1550,47 @@ function brainstormPaths(root, brainstormId) {
|
|
|
1354
1550
|
};
|
|
1355
1551
|
}
|
|
1356
1552
|
|
|
1553
|
+
function discoveryPaths(root, discoveryId) {
|
|
1554
|
+
const dir = path.join(root, ".opennori", "brainstorms");
|
|
1555
|
+
return {
|
|
1556
|
+
jsonPath: path.join(dir, `${discoveryId}.discovery.json`),
|
|
1557
|
+
markdownPath: path.join(dir, `${discoveryId}.discovery.md`)
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function renderDiscoveryMarkdown(discovery) {
|
|
1562
|
+
const lines = [
|
|
1563
|
+
`# ${discovery.id} Acceptance Discovery`,
|
|
1564
|
+
"",
|
|
1565
|
+
"## Goal",
|
|
1566
|
+
"",
|
|
1567
|
+
discovery.goal,
|
|
1568
|
+
"",
|
|
1569
|
+
"## Rule",
|
|
1570
|
+
"",
|
|
1571
|
+
"This is an acceptance discovery source, not a Nori Contract, process plan, or completion evidence.",
|
|
1572
|
+
"",
|
|
1573
|
+
"## Acceptance Gaps",
|
|
1574
|
+
""
|
|
1575
|
+
];
|
|
1576
|
+
|
|
1577
|
+
for (const gap of discovery.gaps) {
|
|
1578
|
+
lines.push(
|
|
1579
|
+
`### ${gap.id}`,
|
|
1580
|
+
"",
|
|
1581
|
+
`Priority: ${gap.priority}`,
|
|
1582
|
+
"",
|
|
1583
|
+
`Question: ${gap.question}`,
|
|
1584
|
+
"",
|
|
1585
|
+
`Why it matters: ${gap.why}`,
|
|
1586
|
+
""
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
lines.push("## Next", "", discovery.next);
|
|
1591
|
+
return `${lines.join("\n")}\n`;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1357
1594
|
function renderBrainstormMarkdown(brainstorm) {
|
|
1358
1595
|
const lines = [
|
|
1359
1596
|
`# ${brainstorm.id} Brainstorm`,
|
|
@@ -1723,6 +1960,10 @@ export async function main(args) {
|
|
|
1723
1960
|
writeManifest(root);
|
|
1724
1961
|
}
|
|
1725
1962
|
|
|
1963
|
+
const nextActions = dryRun
|
|
1964
|
+
? ["Review the upgrade plan, then rerun with --confirm if the planned updates are acceptable."]
|
|
1965
|
+
: ["Run opennori check --root <project> --json to audit existing active Nori Contracts for underspecified ACs before continuing work."];
|
|
1966
|
+
|
|
1726
1967
|
printJson(ok({
|
|
1727
1968
|
root,
|
|
1728
1969
|
dry_run: dryRun,
|
|
@@ -1730,7 +1971,7 @@ export async function main(args) {
|
|
|
1730
1971
|
upgrade_plan: upgradePlan,
|
|
1731
1972
|
actions: upgradePlan.actions,
|
|
1732
1973
|
manifest: dryRun ? buildManifest(root) : safeReadManifest(root)
|
|
1733
|
-
}));
|
|
1974
|
+
}, [], [], nextActions));
|
|
1734
1975
|
return;
|
|
1735
1976
|
}
|
|
1736
1977
|
|
|
@@ -1764,6 +2005,37 @@ export async function main(args) {
|
|
|
1764
2005
|
return;
|
|
1765
2006
|
}
|
|
1766
2007
|
|
|
2008
|
+
if (command === "discover") {
|
|
2009
|
+
const root = resolveRoot(args);
|
|
2010
|
+
const goal = String(argValue(args, "--goal", argValue(args, "--idea", ""))).trim();
|
|
2011
|
+
if (!goal) throw new Error("--goal is required");
|
|
2012
|
+
const discovery = discoverAcceptance(goal, argValue(args, "--id"));
|
|
2013
|
+
const paths = discoveryPaths(root, discovery.id);
|
|
2014
|
+
writeJson(paths.jsonPath, discovery);
|
|
2015
|
+
fs.mkdirSync(path.dirname(paths.markdownPath), { recursive: true });
|
|
2016
|
+
fs.writeFileSync(paths.markdownPath, renderDiscoveryMarkdown(discovery));
|
|
2017
|
+
refreshManifest(root);
|
|
2018
|
+
printJson(ok(
|
|
2019
|
+
{
|
|
2020
|
+
discovery_id: discovery.id,
|
|
2021
|
+
status: discovery.status,
|
|
2022
|
+
goal: discovery.goal,
|
|
2023
|
+
gaps: discovery.gaps,
|
|
2024
|
+
questions: discovery.gaps.map((gap) => gap.question),
|
|
2025
|
+
discovery_path: paths.jsonPath,
|
|
2026
|
+
markdown_path: paths.markdownPath,
|
|
2027
|
+
is_acceptance_contract: false
|
|
2028
|
+
},
|
|
2029
|
+
[
|
|
2030
|
+
{ kind: "acceptance_discovery", path: paths.jsonPath },
|
|
2031
|
+
{ kind: "acceptance_discovery_markdown", path: paths.markdownPath }
|
|
2032
|
+
],
|
|
2033
|
+
[],
|
|
2034
|
+
[discovery.next]
|
|
2035
|
+
));
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1767
2039
|
if (command === "draft") {
|
|
1768
2040
|
const root = resolveRoot(args);
|
|
1769
2041
|
const brainstormId = argValue(args, "--from-brainstorm");
|
|
@@ -1854,12 +2126,23 @@ export async function main(args) {
|
|
|
1854
2126
|
process.exitCode = 1;
|
|
1855
2127
|
return;
|
|
1856
2128
|
}
|
|
2129
|
+
const acceptanceQuality = auditAcceptanceQuality(contract);
|
|
2130
|
+
const warnings = acceptanceQuality.findings.map((finding) => ({
|
|
2131
|
+
type: "acceptance_quality",
|
|
2132
|
+
criterion_id: finding.criterion_id,
|
|
2133
|
+
gap_id: finding.gap_id,
|
|
2134
|
+
message: finding.question
|
|
2135
|
+
}));
|
|
2136
|
+
const nextActions = acceptanceQuality.status === "needs-user-review"
|
|
2137
|
+
? ["Ask the user the acceptance_quality questions, then revise the affected criteria before relying on this contract as complete."]
|
|
2138
|
+
: [];
|
|
1857
2139
|
printJson(ok({
|
|
1858
2140
|
goal_id: contract.goal_id,
|
|
1859
2141
|
workflow_status: ledger.status,
|
|
1860
2142
|
current_gap: currentGap(contract, ledger),
|
|
1861
|
-
statuses: Object.fromEntries(Object.entries(ledger.criteria).map(([id, state]) => [id, state.status]))
|
|
1862
|
-
|
|
2143
|
+
statuses: Object.fromEntries(Object.entries(ledger.criteria).map(([id, state]) => [id, state.status])),
|
|
2144
|
+
acceptance_quality: acceptanceQuality
|
|
2145
|
+
}, [], warnings, nextActions));
|
|
1863
2146
|
return;
|
|
1864
2147
|
}
|
|
1865
2148
|
|