opennori 0.1.6 → 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 +13 -5
- package/README.md +2 -0
- package/examples/opennori-self.json +3 -3
- package/package.json +1 -1
- package/src/cli.js +119 -11
package/.opennori/protocol.md
CHANGED
|
@@ -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
|
|
@@ -249,11 +254,12 @@ On every turn:
|
|
|
249
254
|
8. After approval, run `opennori approve --root <repo> --summary "<approval>" --json`.
|
|
250
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.
|
|
251
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.
|
|
252
|
-
11.
|
|
253
|
-
12.
|
|
254
|
-
13.
|
|
255
|
-
14.
|
|
256
|
-
15.
|
|
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.
|
|
257
263
|
|
|
258
264
|
Useful commands:
|
|
259
265
|
|
|
@@ -269,7 +275,9 @@ Useful commands:
|
|
|
269
275
|
- `opennori profile show --root <repo>`: show profile compliance and blocking items.
|
|
270
276
|
- `opennori list --root <repo>`: list active OpenNori goals.
|
|
271
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.
|
|
272
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.
|
|
273
281
|
- `opennori resume --root <repo>`: recover the active goal, current gap, completion answer, and intervention state.
|
|
274
282
|
- `opennori status --root <repo>`: answer whether the goal is complete and whether the user needs to act.
|
|
275
283
|
- `opennori report --root <repo>`: generate the human acceptance report.
|
package/README.md
CHANGED
|
@@ -94,6 +94,8 @@ language requests to the deterministic CLI state layer.
|
|
|
94
94
|
returns structured JSON for agents and automation.
|
|
95
95
|
- `discover` finds underspecified acceptance gaps before draft, so vague ACs such as "modify fields"
|
|
96
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.
|
|
97
99
|
- `doctor` reports whether project state is `ready`, `needs-action`, or `broken`, with recovery
|
|
98
100
|
actions.
|
|
99
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
|
@@ -39,6 +39,7 @@ const NORI_CAPABILITIES = [
|
|
|
39
39
|
"skill-pack",
|
|
40
40
|
"brainstorm",
|
|
41
41
|
"acceptance-discovery",
|
|
42
|
+
"acceptance-quality-audit",
|
|
42
43
|
"capability-profile",
|
|
43
44
|
"profile-check",
|
|
44
45
|
"archive",
|
|
@@ -428,30 +429,40 @@ function sentenceHasSpecifics(text, terms) {
|
|
|
428
429
|
return terms.some((term) => value.includes(term.toLowerCase()));
|
|
429
430
|
}
|
|
430
431
|
|
|
431
|
-
function
|
|
432
|
-
const text = String(goal || "").trim();
|
|
432
|
+
function discoverAcceptanceGaps(text, { fallback = false, allowedIds = null } = {}) {
|
|
433
433
|
const lowered = text.toLowerCase();
|
|
434
434
|
const gaps = DISCOVERY_GAPS
|
|
435
|
+
.filter((gap) => !allowedIds || allowedIds.has(gap.id))
|
|
435
436
|
.filter((gap) => gap.patterns.some((pattern) => lowered.includes(pattern.toLowerCase())))
|
|
436
437
|
.filter((gap) => {
|
|
437
|
-
if (gap.id === "missing-field-scope") return !sentenceHasSpecifics(text, ["昵称", "头像", "简介", "邮箱", "手机号", "name", "avatar", "bio", "email", "phone"]);
|
|
438
|
-
if (gap.id === "missing-validation-rule") return !sentenceHasSpecifics(text, ["长度", "必填", "格式", "大小", "类型", "字符", "required", "format", "length", "size", "type"]);
|
|
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"]);
|
|
439
440
|
if (gap.id === "missing-success-signal") return !sentenceHasSpecifics(text, ["成功", "保存成功", "成功反馈", "result", "success"]);
|
|
440
441
|
if (gap.id === "missing-persistence-scope") return !sentenceHasSpecifics(text, ["刷新", "重新打开", "重新登录", "跨设备", "refresh", "reload", "reopen", "login"]);
|
|
441
|
-
if (gap.id === "missing-failure-case") return !sentenceHasSpecifics(text, ["网络", "权限", "无效", "错误码", "保留原", "network", "permission", "invalid"]);
|
|
442
|
-
if (gap.id === "missing-out-of-scope-boundary") return !sentenceHasSpecifics(text, ["不在范围", "不包含", "本轮不", "out of scope", "exclude"]);
|
|
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"]);
|
|
443
444
|
if (gap.id === "missing-user-entry") return !sentenceHasSpecifics(text, ["设置页", "登录页", "report", "dashboard", "页面", "page"]);
|
|
444
445
|
if (gap.id === "missing-review-method") return !sentenceHasSpecifics(text, ["截图", "浏览器", "报告", "测试", "review", "screenshot", "browser", "report"]);
|
|
445
446
|
return true;
|
|
446
447
|
});
|
|
447
448
|
|
|
448
|
-
|
|
449
|
+
if (gaps.length > 0 || !fallback) return gaps;
|
|
450
|
+
return [
|
|
449
451
|
{
|
|
450
452
|
id: "missing-review-method",
|
|
451
453
|
question: "用户或评审者应该用什么可复查方式判断这个目标完成?",
|
|
452
454
|
why: "OpenNori 需要先知道完成判断方式,才能形成真正可验收的 AC。"
|
|
453
455
|
}
|
|
454
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 });
|
|
455
466
|
|
|
456
467
|
return {
|
|
457
468
|
protocol_version: "opennori/discovery-v1",
|
|
@@ -469,6 +480,84 @@ function discoverAcceptance(goal, explicitId = undefined) {
|
|
|
469
480
|
};
|
|
470
481
|
}
|
|
471
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
|
+
|
|
472
561
|
function printJson(payload) {
|
|
473
562
|
console.log(JSON.stringify(payload, null, 2));
|
|
474
563
|
}
|
|
@@ -763,16 +852,20 @@ const SKILL_PACK = [
|
|
|
763
852
|
"- Confirm first-time setup after user approval: `opennori bootstrap --root <repo> --confirm --json`.",
|
|
764
853
|
"- Preview install: `opennori install --root <repo> --dry-run --json`.",
|
|
765
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`.",
|
|
766
857
|
"- Preview destructive install: `opennori install --root <repo> --skill --force --dry-run --json`.",
|
|
767
858
|
"- Confirm destructive install: `opennori install --root <repo> --skill --force --confirm --json`.",
|
|
768
859
|
"- Doctor: `opennori doctor --root <repo> --json`.",
|
|
860
|
+
"- Existing contract check after upgrade: `opennori check --root <repo> --json`.",
|
|
769
861
|
"- Preview uninstall: `opennori uninstall --root <repo> --dry-run --json`.",
|
|
770
862
|
"- Remove entry assets while preserving state: `opennori uninstall --root <repo> --confirm --json`.",
|
|
771
863
|
"- Remove all OpenNori state only after explicit user acceptance: `opennori uninstall --root <repo> --include-state --confirm --json`.",
|
|
772
864
|
"",
|
|
773
865
|
"## Rules",
|
|
774
866
|
"Always show dry-run plans before destructive writes.",
|
|
775
|
-
"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."
|
|
776
869
|
]
|
|
777
870
|
},
|
|
778
871
|
{
|
|
@@ -1867,6 +1960,10 @@ export async function main(args) {
|
|
|
1867
1960
|
writeManifest(root);
|
|
1868
1961
|
}
|
|
1869
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
|
+
|
|
1870
1967
|
printJson(ok({
|
|
1871
1968
|
root,
|
|
1872
1969
|
dry_run: dryRun,
|
|
@@ -1874,7 +1971,7 @@ export async function main(args) {
|
|
|
1874
1971
|
upgrade_plan: upgradePlan,
|
|
1875
1972
|
actions: upgradePlan.actions,
|
|
1876
1973
|
manifest: dryRun ? buildManifest(root) : safeReadManifest(root)
|
|
1877
|
-
}));
|
|
1974
|
+
}, [], [], nextActions));
|
|
1878
1975
|
return;
|
|
1879
1976
|
}
|
|
1880
1977
|
|
|
@@ -2029,12 +2126,23 @@ export async function main(args) {
|
|
|
2029
2126
|
process.exitCode = 1;
|
|
2030
2127
|
return;
|
|
2031
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
|
+
: [];
|
|
2032
2139
|
printJson(ok({
|
|
2033
2140
|
goal_id: contract.goal_id,
|
|
2034
2141
|
workflow_status: ledger.status,
|
|
2035
2142
|
current_gap: currentGap(contract, ledger),
|
|
2036
|
-
statuses: Object.fromEntries(Object.entries(ledger.criteria).map(([id, state]) => [id, state.status]))
|
|
2037
|
-
|
|
2143
|
+
statuses: Object.fromEntries(Object.entries(ledger.criteria).map(([id, state]) => [id, state.status])),
|
|
2144
|
+
acceptance_quality: acceptanceQuality
|
|
2145
|
+
}, [], warnings, nextActions));
|
|
2038
2146
|
return;
|
|
2039
2147
|
}
|
|
2040
2148
|
|