@cr1992/agentkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +107 -0
  4. package/README.md +103 -0
  5. package/bin/agentkit.mjs +4 -0
  6. package/bin/cli.mjs +273 -0
  7. package/core/atomic-fs.mjs +23 -0
  8. package/core/cli-help.mjs +54 -0
  9. package/core/content-digest.mjs +66 -0
  10. package/core/digest.mjs +67 -0
  11. package/core/json-schema-lite.mjs +60 -0
  12. package/core/legacy-entry.mjs +37 -0
  13. package/core/reflection.mjs +142 -0
  14. package/core/runtime-bundle.mjs +101 -0
  15. package/docs/loop/embedded-review-adapter.md +41 -0
  16. package/docs/loop/loop-state-machine.md +43 -0
  17. package/docs/loop/recovery-and-fuses.md +34 -0
  18. package/docs/orchestrate/dispatch-contract.md +92 -0
  19. package/docs/orchestrate/failure-routing-and-recovery.md +46 -0
  20. package/docs/orchestrate/host-capability-cache.md +170 -0
  21. package/docs/orchestrate/isolation-fallback.md +18 -0
  22. package/docs/orchestrate/model-routing-config.md +186 -0
  23. package/docs/orchestrate/orchestration-runtime.md +261 -0
  24. package/docs/orchestrate/review-budget.md +90 -0
  25. package/docs/orchestrate/task-playbooks.md +85 -0
  26. package/docs/orchestrate/user-facing-reporting.md +14 -0
  27. package/docs/verify/evidence-schema.md +167 -0
  28. package/docs/verify/input-preparation.md +44 -0
  29. package/docs/verify/verification-protocol.md +76 -0
  30. package/docs/worktree/batch-integration.md +176 -0
  31. package/docs/worktree/delivery-identity.md +41 -0
  32. package/docs/worktree/profile.md +107 -0
  33. package/docs/worktree/reclaim-and-watch.md +96 -0
  34. package/docs/worktree/review-lifecycle.md +92 -0
  35. package/docs/worktree/spawn-and-stack.md +74 -0
  36. package/domains/loop/loop-runtime.mjs +1056 -0
  37. package/domains/orchestrate/contract-tool.mjs +169 -0
  38. package/domains/orchestrate/host_capability_cache.mjs +437 -0
  39. package/domains/orchestrate/orchestration-ledger.mjs +332 -0
  40. package/domains/orchestrate/orchestration-metadata.mjs +4 -0
  41. package/domains/orchestrate/orchestration-reflection.mjs +119 -0
  42. package/domains/orchestrate/resolve_model_policy.mjs +311 -0
  43. package/domains/orchestrate/review-budget.mjs +162 -0
  44. package/domains/orchestrate/worker-capability-preflight.mjs +227 -0
  45. package/domains/verify/verification-runtime.mjs +1638 -0
  46. package/domains/worktree/worktree-archive.mjs +135 -0
  47. package/domains/worktree/worktree-artifact.mjs +123 -0
  48. package/domains/worktree/worktree-batch-integrate.mjs +713 -0
  49. package/domains/worktree/worktree-batch-plan.mjs +198 -0
  50. package/domains/worktree/worktree-batch-result.mjs +241 -0
  51. package/domains/worktree/worktree-core.mjs +908 -0
  52. package/domains/worktree/worktree-doctor.mjs +493 -0
  53. package/domains/worktree/worktree-history.mjs +377 -0
  54. package/domains/worktree/worktree-learning.mjs +110 -0
  55. package/domains/worktree/worktree-lifecycle.mjs +786 -0
  56. package/domains/worktree/worktree-merge-preview.mjs +409 -0
  57. package/domains/worktree/worktree-mgr.mjs +261 -0
  58. package/domains/worktree/worktree-process.mjs +55 -0
  59. package/domains/worktree/worktree-profile.mjs +800 -0
  60. package/domains/worktree/worktree-provider-gitlab.mjs +59 -0
  61. package/domains/worktree/worktree-reclaim.mjs +683 -0
  62. package/domains/worktree/worktree-review-refresh.mjs +574 -0
  63. package/domains/worktree/worktree-review-watch.mjs +661 -0
  64. package/domains/worktree/worktree-scan.mjs +510 -0
  65. package/domains/worktree/worktree-trace-test-worker.mjs +23 -0
  66. package/domains/worktree/worktree-trace.mjs +478 -0
  67. package/manage-worktrees/SKILL.md +87 -0
  68. package/manage-worktrees/agents/openai.yaml +4 -0
  69. package/manage-worktrees/scripts/worktree-mgr.mjs +10 -0
  70. package/manage-worktrees/scripts/worktree-scan.mjs +10 -0
  71. package/orchestrate-subagents/SKILL.md +173 -0
  72. package/orchestrate-subagents/agents/openai.yaml +4 -0
  73. package/orchestrate-subagents/scripts/contract-tool.mjs +10 -0
  74. package/orchestrate-subagents/scripts/host_capability_cache.mjs +10 -0
  75. package/orchestrate-subagents/scripts/orchestration-ledger.mjs +10 -0
  76. package/orchestrate-subagents/scripts/orchestration-reflection.mjs +10 -0
  77. package/orchestrate-subagents/scripts/resolve_model_policy.mjs +10 -0
  78. package/orchestrate-subagents/scripts/review-budget.mjs +10 -0
  79. package/orchestrate-subagents/scripts/worker-capability-preflight.mjs +10 -0
  80. package/package.json +48 -0
  81. package/run-agent-verify-loop/SKILL.md +127 -0
  82. package/run-agent-verify-loop/agents/openai.yaml +4 -0
  83. package/run-agent-verify-loop/scripts/loop-runtime.mjs +10 -0
  84. package/schemas/artifact-ref-v1.schema.json +23 -0
  85. package/schemas/batch-result-v1.schema.json +138 -0
  86. package/schemas/controller-recheck-record-v1.schema.json +22 -0
  87. package/schemas/convergence-report-v1.schema.json +9 -0
  88. package/schemas/effective-worker-capability-v1.schema.json +36 -0
  89. package/schemas/embedded-verification-record-v1.schema.json +32 -0
  90. package/schemas/evidence-package-v1.schema.json +41 -0
  91. package/schemas/improvement-proposal-v1.schema.json +18 -0
  92. package/schemas/loop-state-v1.schema.json +34 -0
  93. package/schemas/model-policy-resolution-v1.schema.json +41 -0
  94. package/schemas/orchestration-ledger-v1.schema.json +110 -0
  95. package/schemas/reflection-record-v1.schema.json +24 -0
  96. package/schemas/review-result-v1.schema.json +37 -0
  97. package/schemas/task-contract-v1.schema.json +83 -0
  98. package/schemas/verification-profile-v1.schema.json +60 -0
  99. package/schemas/worker-capability-requirements-v1.schema.json +21 -0
  100. package/schemas/worktree-binding-v1.schema.json +14 -0
  101. package/shell-manifest.json +79 -0
  102. package/verify-agent-output/SKILL.md +119 -0
  103. package/verify-agent-output/agents/openai.yaml +4 -0
  104. package/verify-agent-output/scripts/verification-runtime.mjs +10 -0
@@ -0,0 +1,67 @@
1
+ // canonical JSON 与 envelope 摘要。
2
+ //
3
+ // 三个 Skill 此前各带一份实现,行为并不完全相同:
4
+ // - orchestrate 版不校验 UTF-16 代理对,也不拒绝非有限 number;
5
+ // - verify 与 loop 版两者都校验,并各自抛自己的 ValidationError 子类。
6
+ // 直接统一会改变其中一侧的失败面,而 canonicalJson 是所有 content_digest 的基础,
7
+ // 摘要语义受方案 §7 保护。因此这里只做实现去重:错误类与严格度由调用方注入,
8
+ // 各 Skill 的可观察行为保持不变。严格度的收敛是一次独立决策,不在本次搬迁里顺手做。
9
+ import { createHash } from 'node:crypto';
10
+
11
+ /** @param {string|Buffer} value */
12
+ export function sha256(value) {
13
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
14
+ }
15
+
16
+ /** @param {Buffer|string} value */
17
+ export function digestBytes(value) {
18
+ return sha256(value);
19
+ }
20
+
21
+ /**
22
+ * @param {{ ValidationError: new (message: string) => Error, strict?: boolean, defaultDigestField?: string }} options
23
+ * strict=true 时校验未配对代理对并拒绝非有限 number(verify / loop 的现有行为)。
24
+ */
25
+ export function createDigestKit({ ValidationError, strict = false, defaultDigestField }) {
26
+ const assertValidUnicode = (value) => {
27
+ for (let index = 0; index < value.length; index += 1) {
28
+ const code = value.charCodeAt(index);
29
+ if (code >= 0xd800 && code <= 0xdbff) {
30
+ const next = value.charCodeAt(index + 1);
31
+ if (!(next >= 0xdc00 && next <= 0xdfff)) throw new ValidationError('字符串含未配对 high surrogate');
32
+ index += 1;
33
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
34
+ throw new ValidationError('字符串含未配对 low surrogate');
35
+ }
36
+ }
37
+ };
38
+
39
+ /** @param {unknown} value */
40
+ function canonicalJson(value) {
41
+ if (value === null) return 'null';
42
+ if (typeof value === 'boolean') return value ? 'true' : 'false';
43
+ if (typeof value === 'number') {
44
+ if (strict && !Number.isFinite(value)) throw new ValidationError('canonical JSON 不接受非有限 number');
45
+ return JSON.stringify(Object.is(value, -0) ? 0 : value);
46
+ }
47
+ if (typeof value === 'string') {
48
+ if (strict) assertValidUnicode(value);
49
+ return JSON.stringify(value);
50
+ }
51
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
52
+ if (typeof value === 'object') {
53
+ const object = /** @type {Record<string, unknown>} */ (value);
54
+ return `{${Object.keys(object).sort().map((key) => `${canonicalJson(key)}:${canonicalJson(object[key])}`).join(',')}}`;
55
+ }
56
+ throw new ValidationError(`canonical JSON 不支持 ${typeof value}`);
57
+ }
58
+
59
+ /** @param {Record<string, unknown>} object @param {string} [digestField] */
60
+ function envelopeDigest(object, digestField = defaultDigestField) {
61
+ const clone = { ...object };
62
+ delete clone[digestField];
63
+ return sha256(Buffer.from(canonicalJson(clone), 'utf8'));
64
+ }
65
+
66
+ return { canonicalJson, envelopeDigest, sha256, assertValidUnicode };
67
+ }
@@ -0,0 +1,60 @@
1
+ // 极简 JSON Schema 子集校验。verify 与 loop 此前各带一份:verify 版收集全部错误后抛第一个,
2
+ // loop 版遍历中遇错即抛。两者遍历顺序相同,抛出的都是同一个「第一个错误」,
3
+ // 因此合并到收集式实现对两侧都是零行为变化;collectJsonSchemaErrors 仅 verify 使用。
4
+ // @ts-check
5
+
6
+ function kind(value) {
7
+ if (value === null) return 'null';
8
+ if (Array.isArray(value)) return 'array';
9
+ if (Number.isInteger(value)) return 'integer';
10
+ return typeof value;
11
+ }
12
+
13
+ function resolveRef(root, ref) {
14
+ if (!ref.startsWith('#/')) throw new Error(`只支持本地 JSON Schema ref: ${ref}`);
15
+ return ref.slice(2).split('/').reduce((value, part) => value?.[part.replaceAll('~1', '/').replaceAll('~0', '~')], root);
16
+ }
17
+
18
+ function collect(value, schema, root, path, errors) {
19
+ if (schema.$ref) return collect(value, resolveRef(root, schema.$ref), root, path, errors);
20
+ if (Object.hasOwn(schema, 'const') && !Object.is(value, schema.const)) errors.push(`${path} 必须等于 ${JSON.stringify(schema.const)}`);
21
+ if (schema.enum && !schema.enum.some((item) => Object.is(item, value))) errors.push(`${path} 不在允许枚举中`);
22
+ if (schema.type) {
23
+ const allowed = Array.isArray(schema.type) ? schema.type : [schema.type];
24
+ const actual = kind(value);
25
+ if (!allowed.includes(actual) && !(actual === 'integer' && allowed.includes('number'))) {
26
+ errors.push(`${path} 类型应为 ${allowed.join('|')},实际 ${actual}`);
27
+ return;
28
+ }
29
+ }
30
+ if (typeof value === 'string') {
31
+ if (schema.minLength !== undefined && value.length < schema.minLength) errors.push(`${path} 长度不足`);
32
+ if (schema.pattern && !new RegExp(schema.pattern, 'u').test(value)) errors.push(`${path} 格式不匹配`);
33
+ }
34
+ if (typeof value === 'number' && schema.minimum !== undefined && value < schema.minimum) errors.push(`${path} 小于最小值`);
35
+ if (Array.isArray(value)) {
36
+ if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(`${path} 项数不足`);
37
+ if (schema.items) value.forEach((item, index) => collect(item, schema.items, root, `${path}[${index}]`, errors));
38
+ }
39
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
40
+ for (const field of schema.required ?? []) if (!Object.hasOwn(value, field)) errors.push(`${path} 缺少 ${field}`);
41
+ const properties = schema.properties ?? {};
42
+ for (const [field, child] of Object.entries(value)) {
43
+ if (properties[field]) collect(child, properties[field], root, `${path}.${field}`, errors);
44
+ else if (schema.additionalProperties === false) errors.push(`${path} 包含未知字段 ${field}`);
45
+ else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') collect(child, schema.additionalProperties, root, `${path}.${field}`, errors);
46
+ }
47
+ }
48
+ }
49
+
50
+ export function collectJsonSchemaErrors(value, schema, label = '$') {
51
+ const errors = [];
52
+ collect(value, schema, schema, label, errors);
53
+ return errors;
54
+ }
55
+
56
+ export function validateJsonSchema(value, schema, label = '$') {
57
+ const errors = collectJsonSchemaErrors(value, schema, label);
58
+ if (errors.length > 0) throw new Error(errors[0]);
59
+ return value;
60
+ }
@@ -0,0 +1,37 @@
1
+ // @ts-check
2
+ // 1.x 兼容入口的唯一实现。Skill 目录下的旧入口只允许调用它,不得包含任何自己的逻辑。
3
+ //
4
+ // domain 模块已经被兼容入口的 `export *` 加载;直接复用其 runCli,避免父子进程各加载一次。
5
+ // 不打印迁移提示:本阶段的门禁是新旧入口完全等价,提示会污染 stderr 比对。
6
+ import { realpathSync } from 'node:fs';
7
+ import { basename, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ import { assertPublicCommandCompatibility, RuntimeBundleError } from './runtime-bundle.mjs';
11
+
12
+ /** @param {string} moduleUrl 调用方的 import.meta.url */
13
+ export function isProcessEntry(moduleUrl) {
14
+ try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(moduleUrl)); }
15
+ catch { return false; }
16
+ }
17
+
18
+ /**
19
+ * 作为进程入口被直接执行时转发给 domain 运行时;被 import 时什么都不做。
20
+ * @param {string} moduleUrl @param {(argv:string[]) => number|undefined} runCli
21
+ * @returns {number|undefined}
22
+ */
23
+ export function forwardLegacyEntry(moduleUrl, runCli) {
24
+ if (!isProcessEntry(moduleUrl)) return undefined;
25
+ const argv = process.argv.slice(2);
26
+ const entryPath = fileURLToPath(moduleUrl);
27
+ const skill = basename(dirname(dirname(entryPath)));
28
+ const entryName = basename(entryPath);
29
+ try {
30
+ assertPublicCommandCompatibility({ skill, entryName, command: argv[0] });
31
+ } catch (error) {
32
+ if (!(error instanceof RuntimeBundleError)) throw error;
33
+ process.stderr.write(`agentkit runtime compatibility error: ${error.message}\n`);
34
+ return 3;
35
+ }
36
+ return runCli(argv);
37
+ }
@@ -0,0 +1,142 @@
1
+ // @ts-check
2
+ // Reflection / Improvement Proposal 的共享构造与校验。
3
+ //
4
+ // 此前 orchestrate 一份、verify 与 loop 各一份(后两者字节相同)。两版不是简单重复,
5
+ // orchestrate 版是严格超集:Bearer 脱敏用 \S+、拒绝输入未知字段、evidence ref 额外做
6
+ // realpath 越界检查。统一到严格版会让 verify/loop 拒绝此前接受的输入,属于行为变更。
7
+ // 这里只做实现去重,严格度由调用方注入并保持各自现状;收敛严格度是一次独立决策。
8
+ import { randomUUID } from 'node:crypto';
9
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
10
+ import { isAbsolute, join, normalize, relative, sep } from 'node:path';
11
+
12
+ import { digestBytes } from './digest.mjs';
13
+
14
+ const DIGEST = /^sha256:[0-9a-f]{64}$/u;
15
+ const TRIGGERS = new Set(['repeated_failure', 'undecidable', 'user_correction', 'runtime_abort', 'workaround', 'protocol_conflict', 'unexpected_outcome', 'terminal_retrospective']);
16
+ const CLASSIFICATIONS = new Set(['contract_gap', 'skill_gap', 'verification_gap', 'tool_gap', 'environment_gap', 'false_positive', 'false_negative', 'inefficiency']);
17
+ const IMPACTS = new Set(['low', 'medium', 'high', 'safety']);
18
+ const CONFIDENCE = new Set(['low', 'medium', 'high']);
19
+ const DISPOSITIONS = new Set(['continue', 'undecidable', 'abort', 're_contract', 'human_gate']);
20
+ const REF_TYPES = new Set(['artifact', 'evidence', 'event', 'diagnostic']);
21
+ const PROBLEM_TYPES = new Set(['skill_gap', 'false_positive', 'false_negative', 'inefficiency']);
22
+ const REFLECTION_INPUT_KEYS = new Set(['contract_digest', 'trigger', 'classification', 'observation', 'evidence_refs', 'impact', 'confidence', 'recommended_disposition']);
23
+ const PROPOSAL_INPUT_KEYS = new Set(['problem_type', 'proposed_change', 'affected_scope', 'counterexamples', 'validation_plan', 'lifecycle']);
24
+ const VALIDATION_PLAN_KEYS = new Set(['replay_cases', 'regression_suites', 'independent_review', 'canary']);
25
+
26
+ // 宽松版只匹配 token 字符类,遇到 `Bearer hidden%token|suffix` 会漏脱敏;严格版用 \S+ 覆盖到空白为止。
27
+ const BEARER_STRICT = /\bBearer\s+\S+/giu;
28
+ const BEARER_LENIENT = /\bBearer\s+[A-Za-z0-9._~+/=-]+/giu;
29
+
30
+ export function safeRelative(id) {
31
+ if (typeof id !== 'string' || !id || isAbsolute(id) || id.includes('\\')) throw new Error('evidence ref id 必须是相对路径');
32
+ const normalized = normalize(id);
33
+ if (normalized === '..' || normalized.startsWith(`..${sep}`)) throw new Error('evidence ref 不能越出 state 目录');
34
+ return normalized;
35
+ }
36
+
37
+ /** @param {{ strict?: boolean }} [options] strict 对应 orchestrate 侧此前的行为。 */
38
+ export function createReflectionKit({ strict = false } = {}) {
39
+ const redact = (text) => {
40
+ if (typeof text !== 'string' || !text.trim()) throw new Error('反思文本不能为空');
41
+ return text
42
+ .replace(strict ? BEARER_STRICT : BEARER_LENIENT, 'Bearer [REDACTED]')
43
+ .replace(/\b(?:ghp|glpat|sk)-[A-Za-z0-9_-]{12,}\b/gu, '[REDACTED]')
44
+ .replace(/\/(?:Users|home)\/[^\s"']+/gu, '[LOCAL_PATH]');
45
+ };
46
+
47
+ const eventDigest = (stateDir, id, parseJsonStrict, canonicalJson) => {
48
+ const match = /^events\.ndjson#revision=(\d+)$/u.exec(id);
49
+ if (!match) return null;
50
+ const lines = readFileSync(join(stateDir, 'events.ndjson'), 'utf8').split('\n').filter(Boolean);
51
+ const event = lines.map((line) => parseJsonStrict(line)).find((candidate) => candidate.revision === Number(match[1]));
52
+ if (!event) throw new Error(`event revision 不存在: ${match[1]}`);
53
+ return digestBytes(Buffer.from(canonicalJson(event), 'utf8'));
54
+ };
55
+
56
+ function verifyEvidenceRefs(stateDir, refs, parseJsonStrict, canonicalJson) {
57
+ if (!Array.isArray(refs)) throw new Error('evidence_refs 必须是数组');
58
+ return refs.map((ref) => {
59
+ if (!ref || !REF_TYPES.has(ref.type) || typeof ref.id !== 'string' || !DIGEST.test(String(ref.digest ?? ''))) throw new Error('evidence ref 结构无效');
60
+ const event = ref.type === 'event' ? eventDigest(stateDir, ref.id, parseJsonStrict, canonicalJson) : null;
61
+ const actual = event ?? (() => {
62
+ const relativeId = safeRelative(ref.id);
63
+ const path = join(stateDir, relativeId);
64
+ if (!existsSync(path)) throw new Error(`evidence ref 不存在: ${ref.id}`);
65
+ if (!strict) return digestBytes(readFileSync(path));
66
+ // 严格版额外解引用 symlink,防止 evidence ref 经软链越出 state 目录。
67
+ const root = realpathSync(stateDir);
68
+ const resolved = realpathSync(path);
69
+ const fromRoot = relative(root, resolved);
70
+ if (fromRoot === '..' || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot)) throw new Error(`evidence ref 越出 state 目录: ${ref.id}`);
71
+ return digestBytes(readFileSync(resolved));
72
+ })();
73
+ if (actual !== ref.digest) throw new Error(`evidence ref digest 不匹配: ${ref.id}`);
74
+ return { type: ref.type, id: ref.id, digest: ref.digest };
75
+ });
76
+ }
77
+
78
+ function buildReflection({ input, stateDir, runDir, scope, skill, parseJsonStrict, canonicalJson, envelopeDigest }) {
79
+ const root = stateDir ?? runDir;
80
+ if (strict) {
81
+ const unknown = Object.keys(input).filter((key) => !REFLECTION_INPUT_KEYS.has(key));
82
+ if (unknown.length) throw new Error(`Reflection 未知字段: ${unknown.sort().join(', ')}`);
83
+ }
84
+ for (const forbidden of ['chain_of_thought', 'reasoning', 'scratchpad']) if (Object.hasOwn(input, forbidden)) throw new Error(`Reflection 禁止字段: ${forbidden}`);
85
+ if (!TRIGGERS.has(input.trigger) || !CLASSIFICATIONS.has(input.classification) || !IMPACTS.has(input.impact) || !CONFIDENCE.has(input.confidence) || !DISPOSITIONS.has(input.recommended_disposition)) throw new Error('Reflection 枚举字段无效');
86
+ const evidenceRefs = verifyEvidenceRefs(root, input.evidence_refs ?? [], parseJsonStrict, canonicalJson);
87
+ if (evidenceRefs.length === 0 && input.confidence !== 'low') throw new Error('无稳定证据的 Reflection 只能是 low confidence');
88
+ const record = {
89
+ schema_version: 1,
90
+ reflection_id: randomUUID(),
91
+ trigger: input.trigger,
92
+ scope,
93
+ affected_skill: skill,
94
+ classification: input.classification,
95
+ observation: redact(input.observation),
96
+ evidence_refs: evidenceRefs,
97
+ impact: input.impact,
98
+ confidence: input.confidence,
99
+ recommended_disposition: input.recommended_disposition,
100
+ recorded_at: new Date().toISOString(),
101
+ };
102
+ record.reflection_digest = envelopeDigest(record, 'reflection_digest');
103
+ return record;
104
+ }
105
+
106
+ function readAndValidateReflection(path, expectedSkill, parseJsonStrict, envelopeDigest) {
107
+ const reflection = parseJsonStrict(readFileSync(path, 'utf8'));
108
+ if (reflection?.schema_version !== 1 || reflection.affected_skill?.name !== expectedSkill || !DIGEST.test(String(reflection.reflection_digest ?? '')) || envelopeDigest(reflection, 'reflection_digest') !== reflection.reflection_digest) throw new Error('Reflection 文件无效或目标 Skill 不匹配');
109
+ return reflection;
110
+ }
111
+
112
+ function buildProposal({ input, reflections, skill, envelopeDigest }) {
113
+ if (strict) {
114
+ const unknown = Object.keys(input).filter((key) => !PROPOSAL_INPUT_KEYS.has(key));
115
+ if (unknown.length) throw new Error(`Proposal 未知字段: ${unknown.sort().join(', ')}`);
116
+ }
117
+ if (input.lifecycle !== undefined && input.lifecycle !== 'proposed') throw new Error('执行 Skill 只能创建 proposed proposal');
118
+ if (!PROBLEM_TYPES.has(input.problem_type)) throw new Error('proposal problem_type 无效');
119
+ if (!Array.isArray(reflections) || reflections.length === 0) throw new Error('proposal 至少绑定一个 Reflection');
120
+ const evidenceRefs = reflections.flatMap((item) => item.evidence_refs ?? []);
121
+ if (evidenceRefs.length === 0) throw new Error('proposal 必须来自带证据的 Reflection');
122
+ const validationPlan = input.validation_plan;
123
+ if (strict && validationPlan && (typeof validationPlan !== 'object' || Array.isArray(validationPlan) || Object.keys(validationPlan).some((key) => !VALIDATION_PLAN_KEYS.has(key)))) throw new Error('proposal validation_plan 含未知字段');
124
+ if (!validationPlan || !Array.isArray(validationPlan.replay_cases) || !Array.isArray(validationPlan.regression_suites) || validationPlan.independent_review !== 'required') throw new Error('proposal validation_plan 无效');
125
+ const proposal = {
126
+ schema_version: 1,
127
+ proposal_id: randomUUID(),
128
+ target_skill: { name: skill.name, based_on_version: skill.version, based_on_digest: skill.content_digest },
129
+ source_reflections: reflections.map((item) => ({ reflection_id: item.reflection_id, reflection_digest: item.reflection_digest })),
130
+ problem: { type: input.problem_type, evidence_refs: evidenceRefs },
131
+ proposed_change: redact(input.proposed_change),
132
+ affected_scope: Array.isArray(input.affected_scope) ? input.affected_scope.map(redact) : [],
133
+ counterexamples: Array.isArray(input.counterexamples) ? input.counterexamples.map(redact) : [],
134
+ validation_plan: { ...validationPlan },
135
+ lifecycle: 'proposed',
136
+ };
137
+ proposal.proposal_digest = envelopeDigest(proposal, 'proposal_digest');
138
+ return proposal;
139
+ }
140
+
141
+ return { buildProposal, buildReflection, readAndValidateReflection, safeRelative, verifyEvidenceRefs };
142
+ }
@@ -0,0 +1,101 @@
1
+ // @ts-check
2
+ // 安装身份真源:把 PATH 上的 CLI、四个 Skill shell 与随包 domain 绑定在一起。
3
+ // 域级 content_digest 负责状态漂移;本模块负责安装不完整或 CLI/shell 版本错配。
4
+ import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
5
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import { distributionDigest } from './content-digest.mjs';
9
+
10
+ const DEFAULT_PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
11
+ const MANIFEST_NAME = 'shell-manifest.json';
12
+ const SKILLS = ['orchestrate-subagents', 'manage-worktrees', 'verify-agent-output', 'run-agent-verify-loop'];
13
+ const HELP_COMMANDS = new Set([undefined, '--help', '-h', 'help', 'capabilities']);
14
+
15
+ export class RuntimeBundleError extends Error {}
16
+
17
+ function readJson(path, label) {
18
+ try { return JSON.parse(readFileSync(path, 'utf8')); }
19
+ catch (error) { throw new RuntimeBundleError(`${label} 无法解析:${error instanceof Error ? error.message : String(error)}`); }
20
+ }
21
+
22
+ function safeExistingPath(packageRoot, value, label) {
23
+ if (typeof value !== 'string' || !value || isAbsolute(value)) throw new RuntimeBundleError(`${label} 必须是包内相对路径`);
24
+ const absolute = resolve(packageRoot, value);
25
+ const lexical = relative(packageRoot, absolute);
26
+ if (!lexical || lexical === '..' || lexical.startsWith(`..${sep}`) || isAbsolute(lexical)) throw new RuntimeBundleError(`${label} 越出包根`);
27
+ if (!existsSync(absolute)) throw new RuntimeBundleError(`${label} 不存在:${value}`);
28
+ const realRoot = realpathSync(packageRoot);
29
+ const real = realpathSync(absolute);
30
+ const resolved = relative(realRoot, real);
31
+ if (resolved === '..' || resolved.startsWith(`..${sep}`) || isAbsolute(resolved)) throw new RuntimeBundleError(`${label} 通过符号链接越出包根`);
32
+ return absolute;
33
+ }
34
+
35
+ export function readShellManifest(packageRoot = DEFAULT_PACKAGE_ROOT) {
36
+ return readJson(resolve(packageRoot, MANIFEST_NAME), MANIFEST_NAME);
37
+ }
38
+
39
+ export function validateShellManifest(packageRoot = DEFAULT_PACKAGE_ROOT) {
40
+ const manifest = readShellManifest(packageRoot);
41
+ const pkg = readJson(resolve(packageRoot, 'package.json'), 'package.json');
42
+ if (manifest.schema_version !== 1) throw new RuntimeBundleError('shell manifest schema_version 必须为 1');
43
+ if (manifest.package_name !== pkg.name || manifest.package_version !== pkg.version) {
44
+ throw new RuntimeBundleError(`CLI/shell 版本不匹配:package ${pkg.name}@${pkg.version},manifest ${manifest.package_name}@${manifest.package_version}`);
45
+ }
46
+ if (manifest.cli?.command !== 'agentkit') throw new RuntimeBundleError('shell manifest CLI 命令必须为 agentkit');
47
+ safeExistingPath(packageRoot, manifest.cli?.entry, 'shell manifest cli.entry');
48
+
49
+ const names = Object.keys(manifest.skills ?? {}).sort();
50
+ if (JSON.stringify(names) !== JSON.stringify([...SKILLS].sort())) throw new RuntimeBundleError('shell manifest 必须且只能声明四件套 Skill');
51
+ for (const skill of SKILLS) {
52
+ const descriptor = manifest.skills[skill];
53
+ if (!descriptor || typeof descriptor.domain !== 'string' || !descriptor.domain) throw new RuntimeBundleError(`${skill} 缺少 domain`);
54
+ safeExistingPath(packageRoot, descriptor.shell, `${skill}.shell`);
55
+ const scriptsDir = resolve(packageRoot, skill, 'scripts');
56
+ safeExistingPath(packageRoot, `${skill}/scripts`, `${skill}.scripts`);
57
+ const actualEntries = readdirSync(scriptsDir).filter((name) => name.endsWith('.mjs')).sort();
58
+ const declaredEntries = Object.keys(descriptor.entries ?? {}).sort();
59
+ if (JSON.stringify(actualEntries) !== JSON.stringify(declaredEntries)) throw new RuntimeBundleError(`${skill} 的兼容入口与 shell manifest 不一致`);
60
+ for (const entryName of declaredEntries) {
61
+ const entry = descriptor.entries[entryName];
62
+ safeExistingPath(packageRoot, entry?.target, `${skill}.${entryName}.target`);
63
+ if (entry.mutates_state !== false && (!Array.isArray(entry.read_only_commands) || entry.read_only_commands.some((item) => typeof item !== 'string' || !item))) {
64
+ throw new RuntimeBundleError(`${skill}.${entryName} 必须声明 read_only_commands 或 mutates_state:false`);
65
+ }
66
+ }
67
+ }
68
+ return { package_name: pkg.name, package_version: pkg.version, manifest };
69
+ }
70
+
71
+ function entryDescriptor(packageRoot, skill, entryName) {
72
+ const manifest = readShellManifest(packageRoot);
73
+ const entry = manifest.skills?.[skill]?.entries?.[entryName];
74
+ if (!entry) throw new RuntimeBundleError(`shell manifest 未声明 ${skill}/scripts/${entryName}`);
75
+ return entry;
76
+ }
77
+
78
+ export function commandMutatesState({ packageRoot = DEFAULT_PACKAGE_ROOT, skill, entryName, command }) {
79
+ if (HELP_COMMANDS.has(command)) return false;
80
+ const entry = entryDescriptor(packageRoot, skill, entryName);
81
+ if (entry.mutates_state === false) return false;
82
+ return !Array.isArray(entry.read_only_commands) || !entry.read_only_commands.includes(command);
83
+ }
84
+
85
+ export function assertPublicCommandCompatibility({ packageRoot = DEFAULT_PACKAGE_ROOT, skill, entryName, command }) {
86
+ if (!commandMutatesState({ packageRoot, skill, entryName, command })) return;
87
+ validateShellManifest(packageRoot);
88
+ }
89
+
90
+ export function runtimeBundleDigest(packageRoot = DEFAULT_PACKAGE_ROOT) {
91
+ const roots = [
92
+ { prefix: 'package', path: packageRoot, entries: ['package.json', 'LICENSE', MANIFEST_NAME] },
93
+ { prefix: 'bin', path: resolve(packageRoot, 'bin') },
94
+ { prefix: 'core', path: resolve(packageRoot, 'core') },
95
+ { prefix: 'schemas', path: resolve(packageRoot, 'schemas') },
96
+ { prefix: 'domains', path: resolve(packageRoot, 'domains') },
97
+ ];
98
+ for (const domain of ['orchestrate', 'worktree', 'verify', 'loop']) roots.push({ prefix: `docs/${domain}`, path: resolve(packageRoot, 'docs', domain) });
99
+ for (const skill of SKILLS) roots.push({ prefix: `skills/${skill}`, path: resolve(packageRoot, skill), entries: ['SKILL.md', 'agents', 'scripts'] });
100
+ return distributionDigest(roots);
101
+ }
@@ -0,0 +1,41 @@
1
+ # Embedded reviewer adapter
2
+
3
+ 本 adapter 只在 freeze 前明确选择 `provider: embedded` 时使用。它定义 standalone Loop 如何准备
4
+ 最小 reviewer 输入、绑定共享 Review Result,以及 embedded assurance 的缺口;不复制完整 verifier
5
+ 行为协议。
6
+
7
+ ## 前置条件
8
+
9
+ 1. 当前 iteration 已绑定完整 Git Artifact Ref;
10
+ 2. workdir clean 且 `HEAD == artifact_sha`;
11
+ 3. `run-embedded-l0` 已完成;
12
+ 4. reviewer 是宿主新上下文,或由用户中继的第二会话;
13
+ 5. reviewer 不可读取实现者过程对话、自述和期待通过的暗示。
14
+
15
+ 只有当前实现者上下文时停止。L2 自查不能满足 embedded L1,也不能产生 pass record。
16
+
17
+ ## Reviewer 输入
18
+
19
+ 只提供冻结 Contract、Profile 中的 L1 lenses、Artifact、L0 入口/结果与去污染 acceptance view。
20
+ reviewer 按 canonical verifier protocol 主动证伪,输出 Review Result v1。
21
+
22
+ `record-embedded-review` 必须校验:
23
+
24
+ - Contract/Profile digest;
25
+ - 当前 loop ID、iteration 与 Artifact;
26
+ - 当前 iteration 的 `challenge_nonce`,拒绝跨轮重放;
27
+ - acceptance ID、finding class 与 evidence;
28
+ - `no_defect_found` forensics;
29
+ - Review Result digest;
30
+ - `host_reported | user_relayed` assurance 和 opaque reviewer run ID。
31
+
32
+ ## 保证边界
33
+
34
+ 生成物固定为 `record_type: embedded_verification_record`,使用 `record_digest`。它缺少标准 verifier 的
35
+ smoke/final 双阶段、内容寻址 Evidence 消费协议和可移植 run provenance,只能推进创建它的 Loop。
36
+
37
+ 必要的结构 schema 位于 [schemas/](../../schemas/);其中 Review Result v1 schema 必须与 verifier Skill 的
38
+ 同版本文件逐字节兼容,但本目录不复制 reviewer 行为协议。
39
+
40
+ 任何 consumer 必须拒绝把 embedded record 当 Evidence Package;不得添加 `evidence_digest`,也不得
41
+ 通过字段重命名或 wrapper 转换提高 assurance。
@@ -0,0 +1,43 @@
1
+ # Loop State v1
2
+
3
+ ## 状态
4
+
5
+ ```text
6
+ active
7
+ ├─ pass + no H gate ───────────────→ completed
8
+ ├─ pass + H gate ──────────────────→ waiting_human
9
+ ├─ fail + 未熔断 ──────────────────→ active(下一 iteration)
10
+ ├─ fail + max/fuse/policy stop ────→ stopped
11
+ ├─ undecidable / blocked_safety ───→ stopped
12
+ └─ verification abort / drift ─────→ stopped
13
+
14
+ waiting_human
15
+ ├─ approved ───────────────────────→ completed
16
+ └─ rejected / explicit stop ───────→ stopped
17
+ ```
18
+
19
+ `completed` 与 `stopped` 是 Loop 终态,不表示外部 Goal 或全局任务完成。
20
+
21
+ ## Iteration
22
+
23
+ 每个 iteration 固定绑定:
24
+
25
+ - 从 1 开始的 index;
26
+ - 完整 Artifact Ref;
27
+ - full provider 的唯一 verification run ID,或 embedded provider 的 L0/Review Result;
28
+ - terminal outcome;
29
+ - Evidence digest 或 embedded record digest;
30
+ - fail 时的稳定 failure signature。
31
+
32
+ 一个 active iteration 尚未终结时不能登记新 Artifact。fail 后由 controller 执行 `next`,再为修复后的
33
+ 新 Artifact 调用 `record-artifact`。
34
+
35
+ ## H gate
36
+
37
+ H gate 与 verification verdict 正交。`pass` 只能把 required gate 推到 `waiting_human`;只有
38
+ `human-gate --decision approved` 能进入 completed。rejected 进入 stopped。
39
+
40
+ ## Verification abort
41
+
42
+ operational abort 不写入 `iterations`,不增加 index,也不生成 failure signature。controller 修复前置
43
+ 条件后,可用 `resume --verification-run-id <new-id>` 为同一 Artifact 创建新 verification run。
@@ -0,0 +1,34 @@
1
+ # 恢复、防重放与熔断
2
+
3
+ ## Journal 与 revision
4
+
5
+ - 每次 mutation 在 state-root lock 内检查 `expected_revision`;
6
+ - event journal 先 fsync 追加完整 next snapshot,再原子替换 snapshot cache;
7
+ - snapshot 缺失或落后时,以最后一个完整 event 为真源恢复;
8
+ - `doctor` 只报告 snapshot、lock 与 Skill drift,不删除或猜测修复状态。
9
+
10
+ ## Evidence 防重放
11
+
12
+ `record-evidence` 持有 state-root 级锁,扫描该 root 下所有 Loop journal 的
13
+ `consumed_verification_run_ids`。run ID 已出现时拒绝;新消费与 Loop next snapshot 写在同一 event,
14
+ 因此 snapshot 写入前崩溃仍能从 journal 发现消费记录。
15
+
16
+ Evidence 还必须绑定当前 Contract digest、Profile digest、Artifact、iteration verification run ID 和
17
+ 自身 digest。只复制 Evidence 文件但未成功追加 event 不构成消费;重试时内容必须一致。
18
+
19
+ ## 失败指纹
20
+
21
+ 只把以下稳定 ID 排序去重后做 canonical JSON SHA-256:
22
+
23
+ - failed L0 `check_id`;
24
+ - L1 `{contract_item_id, class}`。
25
+
26
+ 自然语言 evidence、expected、actual 和 summary 不进入指纹。runtime 不猜测不同文本是否语义同因。
27
+
28
+ 连续相同指纹达到冻结阈值、达到 max iterations、policy=stop、undecidable 或 safety finding 时确定性
29
+ 停止。阈值不能由 implementer 或 reviewer 在运行中提高。
30
+
31
+ ## Skill drift
32
+
33
+ init 冻结本 Skill tree manifest 的 content digest。非终态 mutation 前重算;不一致进入 stopped
34
+ `operational_abort(skill_drift)`,必须由 controller re-contract,不能在旧 Loop 内接受新规则。
@@ -0,0 +1,92 @@
1
+ # 节点派发合同
2
+
3
+ 在派发写 worker、使用完整档、要求独立 Evidence,或进行失败后重派时读取。简单的轻量只读节点只需
4
+ 使用 `SKILL.md` 中的最小合同,不必加载本文件。
5
+
6
+ ## 完整节点 envelope
7
+
8
+ ```yaml
9
+ objective: 唯一目标
10
+ role: scout | worker | critic | judge
11
+ scope:
12
+ include: 允许范围
13
+ exclude: 明确不处理的行动
14
+ inputs: 已知事实、依赖产物与允许假设
15
+ output_contract: 结论格式与交付物
16
+ acceptance: 可观察完成标准
17
+ evidence: 文件、测试、来源或复现步骤
18
+ permissions:
19
+ mode: read_only | write
20
+ writable_paths: []
21
+ environment:
22
+ repository: 主仓绝对路径
23
+ isolation: shared_tree | worktree
24
+ workdir: worker 工作目录绝对路径
25
+ branch: controller 已确认的分支
26
+ runtime: 按需填写共享资源
27
+ dependencies: 前置节点与下游消费者
28
+ required_capabilities: []
29
+ verification:
30
+ requirement: worker_self_check | controller_recheck | independent_evidence | not_applicable
31
+ provider: none | verify-agent-output
32
+ artifact_scope: node_output | integration_candidate | not_applicable
33
+ extensions:
34
+ verification:
35
+ provider: none | verify-agent-output | run-agent-verify-loop
36
+ worktree:
37
+ provider: none | manage-worktrees
38
+ execution:
39
+ orchestration_mode: lightweight | full
40
+ attempt_id: 不透明唯一 ID
41
+ attempt: 从 1 开始
42
+ previous_attempt_id: 首次为 null
43
+ tier: 本地 tier | host-default
44
+ model: 精确模型 ID | inherited | host-default
45
+ reasoning_effort: 精确值 | inherited | unsupported
46
+ adjustment_action: initial | retry_same | raise_effort | switch_model | promote_tier | fresh_context | change_strategy
47
+ failure_kind: null | implementation_defect | reasoning_gap | context_gap | strategy_gap
48
+ failure_ref: null | 前序稳定失败证据摘要
49
+ selection_reason: 模型、强度与节点需求的匹配理由
50
+ config_source: [session | user-host:<path> | host-default]
51
+ configuration_state: user-explicit | session-inferred | session-confirmed | persisted-config | host-default
52
+ model_resolution_state: discovered-and-validated | user-explicit-unverifiable | host-default-unexposed
53
+ capability_source: live-schema | live-schema+effective:<ref> | cache:<path>+live-validation
54
+ capability_fingerprint: 当前宿主能力描述 sha256
55
+ effective_capability_ref: 匹配当前 binding 的记录 | null
56
+ capability_cache_status: fresh | refreshed | absent-write-blocked | stale-write-blocked | not-required-lightweight
57
+ dispatch_provenance: explicit | inherited-controller | host-default
58
+ token_budget: 明确预算 | unsupported
59
+ max_attempts: 包含首次派发的最大尝试数
60
+ stop_conditions: 阻塞、中止和预算退出条件
61
+ ```
62
+
63
+ 完整档先通过 `contract-tool.mjs normalize / validate / digest / review-view / diff` 冻结公共 Task
64
+ Contract,再把实际宿主派发回执写入 `dispatch-record`。schema 和 runtime 命令见
65
+ [orchestration-runtime.md](orchestration-runtime.md)。
66
+
67
+ ## 合同不变量
68
+
69
+ - `objective / scope / output_contract / acceptance / evidence / verification` 不得缺失;写任务还必须有
70
+ `permissions / writable_paths / environment`。
71
+ - `inputs` 中的已知事实必须由 controller 亲自核实并能指出证据;单一来源解析、模式匹配或记忆值属于
72
+ 允许假设,并注明 worker 使用前必须验证。
73
+ - `exclude` 禁止未申报行动,不禁止带理由上报越界方案;外部可变状态在动作前重查。
74
+ - `not_applicable` 只给 `role: critic | scout` 的只读评审节点,必须搭配 `provider: none` 与
75
+ `artifact_scope: not_applicable`;这类节点验收时附一份 report 即可,不冒充实现交付。
76
+ - `independent_evidence` 只使用 `verify-agent-output`,且 provider 与 Skill identity 必须在公共合同
77
+ freeze 前声明。通常只对最终冻结的高风险候选启用,不给普通 worker 预先升级。
78
+ - 修复类 acceptance 写从裁决真源推导的预期态,不把 reviewer 处方或修复手段本身当标准。
79
+ - 重派的 `attempt` 连续,绑定直接前序 attempt 与稳定失败证据,不得覆盖旧节点。
80
+
81
+ ## Worker 状态包
82
+
83
+ ```yaml
84
+ status: completed | partial | blocked
85
+ conclusion: 核心结论
86
+ evidence: 可核验证据
87
+ changes: 实际变更
88
+ risks: 未解决风险
89
+ next_action: 建议后续动作
90
+ ```
91
+
92
+ 该状态包只是 controller 的验收输入,不是业务交付,也不能自行宣布全局完成。