aimship 0.1.0-preview.11 → 0.1.0-preview.13

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 (46) hide show
  1. package/CONTRIBUTING.md +3 -3
  2. package/README.md +6 -10
  3. package/README.zh-CN.md +6 -10
  4. package/docs/cli.md +23 -25
  5. package/docs/guide.md +314 -148
  6. package/docs/guide.zh-CN.md +314 -148
  7. package/lib/approvals.mjs +77 -13
  8. package/lib/cli.mjs +27 -21
  9. package/lib/delivery.mjs +15 -12
  10. package/lib/discovery.mjs +25 -0
  11. package/lib/documents.mjs +159 -65
  12. package/lib/install.mjs +1 -1
  13. package/lib/language.mjs +41 -0
  14. package/lib/location.mjs +65 -0
  15. package/lib/state.mjs +54 -87
  16. package/lib/view.mjs +36 -34
  17. package/package.json +1 -1
  18. package/skills/aimship/SKILL.md +3 -3
  19. package/skills/aimship/references/artifact-map.json +165 -9
  20. package/skills/aimship/references/core.md +17 -13
  21. package/skills/aimship/references/document-patterns.md +17 -11
  22. package/skills/aimship/references/document-review.md +1 -3
  23. package/skills/aimship/references/hosts.md +11 -9
  24. package/skills/aimship/references/recovery.md +9 -13
  25. package/skills/aimship/references/sources-and-changes.md +11 -4
  26. package/skills/aimship/references/stages/diagnose.md +1 -1
  27. package/skills/aimship/references/stages/frame.md +1 -1
  28. package/skills/aimship/references/stages/handoff.md +1 -1
  29. package/skills/aimship/references/stages/implement.md +2 -2
  30. package/skills/aimship/references/stages/plan.md +1 -1
  31. package/skills/aimship/references/stages/requirements.md +1 -1
  32. package/skills/aimship/references/stages/review.md +1 -1
  33. package/skills/aimship/references/stages/scope.md +1 -1
  34. package/skills/aimship/references/stages/tech-design.md +1 -1
  35. package/skills/aimship/references/stages/test-design.md +1 -1
  36. package/skills/aimship/references/stages/verify.md +2 -2
  37. package/skills/aimship/references/templates/change-log.md +0 -2
  38. package/skills/aimship/references/templates/diagnosis.md +0 -7
  39. package/skills/aimship/references/templates/handoff.md +0 -6
  40. package/skills/aimship/references/templates/plan.md +2 -8
  41. package/skills/aimship/references/templates/prd.md +0 -7
  42. package/skills/aimship/references/templates/review.md +0 -3
  43. package/skills/aimship/references/templates/scope.md +0 -7
  44. package/skills/aimship/references/templates/tech-design.md +0 -7
  45. package/skills/aimship/references/templates/testing.md +2 -8
  46. package/lib/migrate.mjs +0 -77
package/lib/approvals.mjs CHANGED
@@ -1,12 +1,11 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
- import { artifactMap, safeRead, anchors } from './documents.mjs';
2
+ import { artifactMap, safeRead, hasLocator, sections } from './documents.mjs';
3
3
  import { fail, hash } from './io.mjs';
4
4
 
5
5
  const text = value => typeof value === 'string' && value.trim().length > 0;
6
6
  const reject = message => fail('REVIEW_CHECK', message, 'Read review status. Preserve history, self-check the current document, and record only an actual explicit user decision.');
7
7
  const kinds = ['self-check', 'approved', 'changes-requested', 'editorial'];
8
8
  export function validateReviewState(state) {
9
- if (state.reviewPolicy === undefined) return;
10
9
  if (state.reviewPolicy !== 1 || !Array.isArray(state.reviewHistory)) reject('Unsupported review policy or missing reviewHistory.');
11
10
  const ids = new Set();
12
11
  for (const r of state.reviewHistory) {
@@ -27,8 +26,9 @@ export async function reviewFingerprint(directory, state, doc) {
27
26
  let body = await safeRead(directory, state, doc.path, stop ? 'utf8' : null);
28
27
  if (body === null) return null;
29
28
  if (stop) {
30
- const at = body.search(new RegExp(`<a\\s+id=["']${stop}["']\\s*><\\/a>`));
31
- if (at < 0) return null;
29
+ const boundary=sections(body,(await artifactMap()).documents[doc.kind]).find(s=>s.id===stop);
30
+ if(boundary?.count!==1) return null;
31
+ const at=boundary.start;
32
32
  body = body.slice(0, at);
33
33
  }
34
34
  const assets = [];
@@ -49,8 +49,50 @@ function decisionFor(state, doc, digest) {
49
49
  if (approved?.version !== doc.version || approved?.digest !== digest) approved = null;
50
50
  return { approved, checked, latest };
51
51
  }
52
+ // A corrective review may consume a failed conclusion without first pretending
53
+ // it passed. Its recorded coverage must include the object it supersedes.
54
+ function reviewsResult(later, earlier) {
55
+ return later.stage === 'review' && earlier.stage === 'review' && text(earlier.resultRef)
56
+ && later.historyResultRefs?.includes(earlier.resultRef)
57
+ && (!earlier.overall || later.overall === true)
58
+ && (earlier.reviewOf?.length
59
+ ? earlier.reviewOf.every(id => later.reviewOf?.includes(id))
60
+ : !later.reviewOf?.length && later.scope === earlier.scope);
61
+ }
62
+ // Required edges for standard delivery are derived from the actual work, not
63
+ // optional inputRefs/dependsOn. Limited commissions retain their selected graph.
64
+ function standardEntry(state, run, index, completed) {
65
+ const before=state.stageRuns.slice(0,index), dependsOn=[], issues=[];
66
+ const status=id=>completed.find(r=>r.id===id);
67
+ const implementations=before.filter(r=>r.stage==='implement');
68
+ if(run.stage==='review') {
69
+ const covered=run.reviewOf??[];
70
+ if(!covered.length) issues.push('entry:implementation-coverage');
71
+ dependsOn.push(...covered);
72
+ if(run.overall && implementations.some(r=>!covered.includes(r.id))) issues.push('entry:overall-coverage');
73
+ for(const id of covered) {
74
+ const implementation=state.stageRuns.find(r=>r.id===id);
75
+ if(['failed','mixed','unknown','not-run'].includes(implementation?.result)) issues.push(`entry:implementation-result:${id}`);
76
+ }
77
+ } else if(['verify','handoff'].includes(run.stage)) {
78
+ if(!implementations.length) issues.push('entry:implementation');
79
+ dependsOn.push(...implementations.map(r=>r.id),...before.filter(r=>r.stage==='review').map(r=>r.id));
80
+ const overall=before.findLast(r=>r.stage==='review' && r.overall && r.result==='passed' && status(r.id)?.releaseReady && implementations.length && implementations.every(b=>r.reviewOf?.includes(b.id) && before.indexOf(b)<before.indexOf(r)));
81
+ if(!overall) issues.push('entry:passing-overall-review');
82
+ if(run.stage==='handoff') {
83
+ const checks=before.filter(r=>r.stage==='verify');
84
+ const effective=checks.filter((r,i)=>!checks.slice(i+1).some(later=>status(later.id)?.releaseReady && later.result==='passed' && later.historyResultRefs?.includes(r.resultRef)));
85
+ if(!effective.length) issues.push('entry:verification');
86
+ dependsOn.push(...checks.map(r=>r.id));
87
+ for(const check of effective) {
88
+ if(check.result!=='passed') issues.push(`entry:verification-result:${check.id}`);
89
+ if(overall && before.indexOf(check)<before.indexOf(overall)) issues.push(`entry:verification-before-review:${check.id}`);
90
+ }
91
+ }
92
+ }
93
+ return {dependsOn,issues};
94
+ }
52
95
  export async function reviewStatus(directory, state) {
53
- if (state.reviewPolicy !== 1) return { compatibility: 'review-adoption-required', canProceed: false, pending: [], runs: [], documents: [], instruction: 'Preserve old results. Enable reviewPolicy: 1 with an empty reviewHistory after compatibility assessment; no historical approval is inferred.' };
54
96
  const map = await artifactMap(), documents = [], evidenceIssues = [], evidenceCache = new Map();
55
97
  for (const r of state.reviewHistory ?? []) {
56
98
  if (!evidenceCache.has(r.sourceRef)) { try { evidenceCache.set(r.sourceRef,await evidence(directory,state,r.sourceRef)); } catch { evidenceCache.set(r.sourceRef,null); } }
@@ -63,6 +105,11 @@ export async function reviewStatus(directory, state) {
63
105
  const { approved, checked, latest } = decisionFor(state, doc, digest);
64
106
  documents.push({ path: doc.path, version: doc.version, digest, selfCheck: checked?.id ?? null, approval: approved?.id ?? null, status: !digest ? 'unavailable' : approved && (evidenceIssues.includes(approved.id) || (checked && evidenceIssues.includes(checked.id))) ? 'evidence-changed' : approved ? 'approved' : latest?.kind === 'changes-requested' ? 'changes-requested' : latest?.kind === 'self-check' && latest.result === 'failed' ? 'self-check-failed' : checked ? 'pending' : 'self-check-required' });
65
107
  }
108
+ let limitedScope=false;
109
+ if(state.deliveryPolicy?.partialRef) {
110
+ try { await evidence(directory,state,state.deliveryPolicy.partialRef); limitedScope=true; }
111
+ catch { /* Report an unavailable scope basis; never silently grant an exception. */ }
112
+ }
66
113
  const runs = [], requirement = state.documentPolicy?.authority === 'adopted' ? 'scope' : 'prd';
67
114
  for (const [i, run] of state.stageRuns.entries()) {
68
115
  const spec = map.stages[run.stage];
@@ -75,11 +122,19 @@ export async function reviewStatus(directory, state) {
75
122
  const producers = state.stageRuns.slice(0, i).filter(r => r.outputRefs.some(out => run.inputRefs.some(input => out.split('#')[0] === input.split('#')[0]))).map(r => r.id);
76
123
  const development = ['requirements','scope','tech-design','contract','test-design','plan','implement'];
77
124
  const prerequisites = development.includes(run.stage) ? state.stageRuns.slice(0,i).filter(r=>development.includes(r.stage) && development.indexOf(r.stage)<development.indexOf(run.stage)).map(r=>r.id) : [];
78
- const dependsOn = [...new Set([...declared, ...producers, ...prerequisites])];
79
- const blockedBy = dependsOn.filter(id => !runs.find(r => r.id === id)?.ready);
125
+ const gated=state.deliveryPolicy?.version===1 && ['review','verify','handoff'].includes(run.stage);
126
+ const entrance=gated && !limitedScope ? standardEntry(state,run,i,runs) : {dependsOn:[],issues:[]};
127
+ if(gated && state.deliveryPolicy.partialRef && !limitedScope) entrance.issues.push('entry:limited-scope-evidence');
128
+ const dependsOn = [...new Set([...declared, ...producers, ...prerequisites,...entrance.dependsOn])];
129
+ const blockedBy = dependsOn.filter(id => {
130
+ const dependency = runs.find(r => r.id === id);
131
+ const original = state.stageRuns.find(r => r.id === id);
132
+ return !dependency?.releaseReady && !(dependency?.ready && reviewsResult(run, original));
133
+ });
134
+ blockedBy.push(...entrance.issues);
80
135
  // An omitted producer run must not turn an unapproved input into permission.
81
- // Schema 1/2 retain their historical arrangement; new initialization uses schema 3.
82
- if (state.schemaVersion === 3) {
136
+ // Schema 4 keeps prerequisite approvals even when producer runs are omitted.
137
+ if (state.schemaVersion === 4) {
83
138
  const needs = [];
84
139
  if (['prototype','tech-design','contract','test-design','plan','implement'].includes(run.stage)) needs.push(requirement);
85
140
  if (['contract','test-design','plan','implement'].includes(run.stage)) needs.push('tech-design');
@@ -97,13 +152,22 @@ export async function reviewStatus(directory, state) {
97
152
  }
98
153
  const waiting = required.filter(file => documents.find(d => d.path === file)?.status !== 'approved');
99
154
  const ready = run.progress === 'done' && !['needs-review','unknown'].includes(run.applicability) && !blockedBy.length && !waiting.length && (!spec?.humanReview || required.length > 0);
100
- runs.push({ id: run.id, stage: run.stage, progress: run.progress, applicability: run.applicability, documents: required, waiting, dependsOn, blockedBy, ready });
155
+ const releaseReady = ready && (run.stage !== 'review' || run.result === 'passed');
156
+ runs.push({ id: run.id, stage: run.stage, progress: run.progress, applicability: run.applicability, documents: required, waiting, dependsOn, blockedBy, ready, releaseReady });
157
+ if (releaseReady && run.stage === 'review') {
158
+ for (const earlier of runs.slice(0,-1)) {
159
+ if (earlier.ready && reviewsResult(run, state.stageRuns.find(r=>r.id===earlier.id))) {
160
+ earlier.releaseReady = true;
161
+ earlier.resolvedBy = run.id;
162
+ }
163
+ }
164
+ }
101
165
  }
102
166
  const current = runs.find(r => r.id === state.currentRun);
103
167
  const pending = runs.filter(r => r.progress !== 'planned').flatMap(r => r.waiting.map(document => ({ run: r.id, document, status: documents.find(d => d.path === document)?.status })));
104
168
  const nextRun = runs.find(r => !r.ready);
105
169
  const nextStage = current ? runs[runs.indexOf(current) + 1]?.stage ?? null : nextRun?.stage ?? null;
106
- return { compatibility: 'current', documents, runs, pending, evidenceIssues, currentRun: current?.id ?? null, nextRun: nextRun?.id ?? null, nextStage, canProceed: current ? !current.blockedBy.length && !(current.progress === 'done' && !current.ready) && current.progress !== 'blocked' && !['needs-review','unknown'].includes(current.applicability) : !pending.length && !runs.some(r=>r.progress==='blocked' || (r.progress !== 'planned' && r.blockedBy.length) || ['needs-review','unknown'].includes(r.applicability)), identity: 'Record validation only; user identity, intent and source authenticity are not authenticated.' };
170
+ return { compatibility: 'current', documents, runs, pending, evidenceIssues, currentRun: current?.id ?? null, nextRun: nextRun?.id ?? null, nextStage, canProceed: current ? !current.blockedBy.length && !(current.progress === 'done' && !current.releaseReady) && current.progress !== 'blocked' && !['needs-review','unknown'].includes(current.applicability) : !pending.length && !runs.some(r=>r.progress==='blocked' || (r.progress === 'done' && !r.releaseReady) || (r.progress !== 'planned' && r.blockedBy.length) || ['needs-review','unknown'].includes(r.applicability)), identity: 'Record validation only; user identity, intent and source authenticity are not authenticated.' };
107
171
  }
108
172
  async function evidence(directory, state, ref) {
109
173
  if (!text(ref) || ref.includes('\\') || ref.split('#')[0].split('/').includes('..') || ref.startsWith('/') || (/^[a-zA-Z]:/.test(ref) && !ref.startsWith('repo:'))) reject('Review evidence must stay inside a registered root.');
@@ -111,12 +175,12 @@ async function evidence(directory, state, ref) {
111
175
  try { body = await safeRead(directory, state, ref); } catch { reject(`Cannot read review evidence ${ref}.`); }
112
176
  if (!text(body)) reject(`Preserve locally readable review evidence for ${ref}.`);
113
177
  const fragment = ref.split('#')[1];
114
- if (fragment && !anchors(body).includes(fragment)) reject(`Review evidence anchor not found: ${ref}.`);
178
+ if (fragment && !hasLocator(body,fragment,(await artifactMap()).documents[state.documents?.find(d=>d.mode==='new' && d.path===ref.split('#')[0])?.kind])) reject(`Review evidence locator not found: ${ref}.`);
115
179
  return hash(body);
116
180
  }
117
181
  export async function enforceReviews(directory, state, previous) {
118
182
  validateReviewState(state);
119
- if (state.reviewPolicy !== 1) reject('Adopt the review protocol before workflow updates; old results remain readable but are not approvals.');
183
+ if (state.reviewPolicy !== 1) reject('Invalid review policy.');
120
184
  const oldHistory = previous?.reviewHistory ?? [];
121
185
  for (const old of previous?.documents ?? []) if (oldHistory.some(r=>r.document===old.path) && !state.documents.some(d=>d.path===old.path && d.kind===old.kind)) reject(`Preserve reviewed document registration ${old.path}.`);
122
186
  if (state.reviewHistory.length < oldHistory.length || !oldHistory.every((r, i) => isDeepStrictEqual(r, state.reviewHistory[i]))) reject('Preserve review history unchanged and in order.');
package/lib/cli.mjs CHANGED
@@ -1,4 +1,6 @@
1
- import { migrateState, scanDeliveries } from './migrate.mjs';
1
+ import { pathToFileURL } from 'node:url';
2
+ import { text } from './language.mjs';
3
+ import { scanDeliveries } from './discovery.mjs';
2
4
  import { inspectDocuments } from './documents.mjs';
3
5
  import fs from 'node:fs/promises';
4
6
  import path from 'node:path';
@@ -11,17 +13,14 @@ const HELP = {
11
13
  usage: 'node <aimship.mjs> <command> [delivery-directory] [options]',
12
14
  commands: {
13
15
  render: 'render <dir> --revision <N> --rules-version <version> [--preserve-edits]. Explicitly rebuild status.md; preserve manual edits before replacement. Does not replay a state update.',
14
- migrate: 'migrate <dir> --revision <old-N> --rules-version <active-version> --input <json-file|->. Input {compatibilityRef}; preserve legacy state then move to schema 2. Adopt rules separately; documents require reviewed reconciliation.',
15
- scan: 'scan <material-root>. Read immediate delivery directories in both layouts, deduplicate IDs and report conflicts/unknown records. No global search or write.',
16
+ scan: 'scan <material-root>. Read immediate delivery directories at .aimship/state.json, deduplicate IDs and report conflicts or invalid records. No global search or write.',
16
17
  status: 'status [--home <dir>]. Read only Aimship-managed paths and fixed entries. Report registration, shared entry presence/integrity, active version, pending work and config problems. Host loading, login and access permissions remain unverified; no directories are created.',
17
18
  bind: 'Resolve active installation once. Returns immutable release/core/stage/CLI paths plus configured dataRoot, configPath and customPath. Source mode returns the default data root with null config/custom paths. No directories are created. Optional --home is for isolated testing.',
18
- init: 'init <dir> --input <json-file|-> --rules-version <version>. Create state once; input requires goal.summary and saved source/request.md; endpoint and artifact conditions may remain null (unknown). Does not create templates.',
19
- read: 'read <dir> [--rules-version <version>] [--delivery-id <known-id>]. Read-only JSON access; unknown schemas remain readable.',
19
+ init: 'init <dir> --input <json-file|-> --rules-version <version>. Create state once; input requires goal.summary, language (en or zh-CN) and saved source/request.md; directory must be a child of bound dataRoot unless storage has an explicit local decisionRef; endpoint and artifact conditions may remain null (unknown). Does not create templates.',
20
+ read: 'read <dir> [--rules-version <version>] [--delivery-id <known-id>]. Read current delivery state without executing work.',
20
21
  update: 'update <dir> --revision <N> --rules-version <version> --input <json-file|->. Recursively merge objects; arrays replace in full. Identity/history fields are managed. null is a literal value, not deletion.',
21
- check: 'check <dir> [--rules-version <version>] [--all]. Check structure and current input references (or every registered reference). Checks explicit anchors/identity metadata in new local Markdown; reused and unsupported fragments are explicitly not checked. Does not prove semantics, remote access or validation results.',
22
+ check: 'check <dir> [--rules-version <version>] [--all]. Check structure and current input references (or every registered reference). Checks ordinary Markdown sections and searchable numbers in new local Markdown; reused and unsupported fragments are explicitly not checked. Does not prove semantics, remote access or validation results.',
22
23
  resume: 'resume <dir> [--rules-version <version>] [--delivery-id <known-id>]. Read state and return fixed rule paths, required inputs, warnings, and continuation text; never execute work or rewrite state.',
23
- checkpoint: 'checkpoint <dir> --revision <N> --rules-version <old-version> --input <json-file|->. Save only checkpoint {summary, ref?} and next when a version mismatch interrupts ongoing work. Cannot complete work.',
24
- adopt: 'adopt <dir> --revision <N> --rules-version <active-version> --input <json-file|->. Input is {compatibilityRef}; records the assessed stable-point rule switch without changing business history.',
25
24
  recover: 'recover <dir> --revision <backup-revision> --rules-version <version>. Restore state.previous.json only over missing/corrupt state; preserve corrupted bytes. Inspect actual artifacts afterward; nothing reruns.',
26
25
  install: 'install --host <codex|cursor|claude-code> [--host ...] [--data-root <dir>] [--home <dir>]. Explicitly copy the whole release into .aimship. Repeated installs retain registered hosts. Installed tools default to their owning home; explicit --home overrides it for isolated testing. Data root is configured, never created.',
27
26
  uninstall: 'uninstall [--host <host> ...] [--home <dir>]. --host disconnects selected hosts, retaining shared core even when no hosts remain. Omit --host for full product uninstall. Delivery data, config, custom and modified/unowned files are retained. Installed tools default to their owning home; explicit --home overrides it.',
@@ -30,18 +29,20 @@ const HELP = {
30
29
  stateProtocol: {
31
30
  schemaVersion: SCHEMA_VERSION,
32
31
  documentReview: {reviewPolicy:1, reviewHistory:'Append-only {id,kind,document,version,digest,sourceRef,summary}. self-check requires result: passed|failed; approved requires decision: explicit-approval; editorial requires previousDigest. Read review.documents for current digests. Tools validate records, not user identity. See docs/cli.md#document-review-protocol.'},
33
- deliveryPolicy: 'New states default to {version:1}; partialRef identifies an explicitly limited scope, null restores standard scope with a new goal decision. Legacy absence remains historical-unassessed; explicit adoption requires compatibilityRef. Completion is derived separately from goal/acceptance.',
32
+ deliveryPolicy: 'New states default to {version:1}; partialRef identifies an explicitly limited scope, null restores standard scope with a new goal decision. Completion is derived separately from goal/acceptance.',
34
33
  sessions: 'run.sessionGroup is an advisory context group, auto-filled for new runs; reviewOf names implementation runs and overall:true identifies overall review. read/check/resume expose session/completion; writeResult.session is withheld on failed view saving. No session gate or automatic execution.',
35
34
  stages: STAGES,
36
35
  progress: ['planned', 'active', 'blocked', 'done'],
37
36
  result: ['passed', 'failed', 'not-run', 'unknown', 'mixed'],
38
37
  applicability: ['current', 'needs-review', 'unknown'],
39
38
  outcome: { goal: ['in-progress', 'met', 'not-met', 'unknown'], acceptance: ['not-recorded', 'accepted', 'declined', 'unknown'] },
40
- stageInstance: { id: 'stable-instance-id', stage: '<stage>', scope: 'batch or whole-delivery scope', progress: 'planned', inputRefs: [], outputRefs: [], resultRef: 'review.md#rev-001-v1 (required when done)' },
39
+ stageInstance: { id: 'stable-instance-id', stage: '<stage>', scope: 'batch or whole-delivery scope', progress: 'planned', inputRefs: [], outputRefs: [], resultRef: 'review.md#REV-001 (required when done)' },
41
40
  references: 'delivery-relative/path#optional-fragment, repo:<registered-id>/<repo-relative-path>#fragment, or HTTP(S) URL. Local paths cannot escape their root. Repository registration: {id, path: absolute-path, role}.',
42
41
  topLevel: {
43
- managed: 'schemaVersion, revision, deliveryId, rulesVersion, createdAt, updatedAt, goalHistory, rulesHistory, documentDigests, identityHistory, retiredIdentities, legacyRunIds, writeResult. Do not supply these in init/update.',
44
- requiredInit: 'goal: {summary, endpoint?}; save a nonempty source/request.md before init. Defaults: endpoint null, goalRef source/request.md, empty documents, and null documentPolicy conditions/authority. Register PRD/Scope and switch goalRef when drafted; no empty templates.',
42
+ managed: 'schemaVersion, revision, deliveryId, rulesVersion, createdAt, updatedAt, goalHistory, documentDigests, writeResult. Do not supply these in init/update.',
43
+ requiredInit: 'language: en|zh-CN and goal: {summary, endpoint?}; save a nonempty source/request.md before init. Defaults: endpoint null, goalRef source/request.md, empty documents, and null documentPolicy conditions/authority. Register PRD/Scope and switch goalRef when drafted; no empty templates.',
44
+ language: 'en or zh-CN from explicit user preference, otherwise this delivery conversation. Required in current state; retained on update/render/resume.',
45
+ storage: 'Init-only {mode: explicit, decisionRef: delivery-relative-readable-file} for a user-selected location. Otherwise a strict child of configured dataRoot. Returned dataRoot is managed; ordinary workspace/material location is not a choice.',
45
46
  stageRuns: 'Ordered array of stageInstance objects, default []. Every ID is unique; currentRun must match one of these IDs or be null. Stage types may repeat with distinct IDs.',
46
47
  currentRun: 'Instance ID or null (default). Do not invent an ID without registering its instance in stageRuns.',
47
48
  repositories: 'Array of {id, path: absolute-path, role, ...optional-version-facts}, default []. No repository is required for a design-only goal.',
@@ -52,6 +53,7 @@ const HELP = {
52
53
  closureRefs: 'Historical closure references, default []. Preserve them across reopenings.'
53
54
  },
54
55
  minimalInit: {
56
+ language: 'en',
55
57
  goal: { summary: 'Export the currently filtered orders', endpoint: null },
56
58
  goalRef: 'source/request.md',
57
59
  documentPolicy: {authority:null,interaction:null,prototypeCovered:null,interface:null,databases:null},
@@ -117,11 +119,14 @@ async function binding(runtime) {
117
119
  if (configPath) {
118
120
  dataRoot = (await readConfig(configPath)).dataRoot;
119
121
  }
120
- return { version: pkg.version, schemaVersion: SCHEMA_VERSION, launcherProtocol: 1, releaseRoot: root, corePath, stageDirectory: path.join(root, 'skills', 'aimship', 'references', 'stages'), cliPath: path.join(root, 'bin', 'aimship.mjs'), launcherPath: runtime.launcher, nodePath: process.execPath, dataRoot, configPath, customPath: runtime.installRoot ? path.join(runtime.installRoot, 'custom') : null, configurationStatus: runtime.installRoot ? 'installed' : 'source-default' };
122
+ const protocolPath=path.join(root,'lib/state.mjs'); await noSymlinks(protocolPath);
123
+ const targetProtocol = await import(pathToFileURL(protocolPath).href);
124
+ if (!Number.isSafeInteger(targetProtocol.SCHEMA_VERSION) || targetProtocol.SCHEMA_VERSION<1) fail('INCOMPLETE_RELEASE','Bound release does not export its state protocol.','Repair the exact release before creating state.');
125
+ return { version: pkg.version, schemaVersion: targetProtocol.SCHEMA_VERSION, toolVersion:runtime.version, toolSchemaVersion:SCHEMA_VERSION, launcherProtocol: 1, releaseRoot: root, corePath, stageDirectory: path.join(root, 'skills', 'aimship', 'references', 'stages'), cliPath: path.join(root, 'bin', 'aimship.mjs'), launcherPath: runtime.launcher, nodePath: process.execPath, dataRoot, configPath, customPath: runtime.installRoot ? path.join(runtime.installRoot, 'custom') : null, configurationStatus: runtime.installRoot ? 'installed' : 'source-default' };
121
126
  }
122
127
  function allowed(command, options) {
123
128
  const common = ['help', 'version'];
124
- const perCommand = { render: ['revision','rules-version','preserve-edits','home'], migrate:['revision','rules-version','input','home'], scan:['home'], status: ['home'], bind: ['home'], init: ['input', 'rules-version', 'home'], read: ['rules-version', 'delivery-id', 'home'], update: ['input', 'rules-version', 'revision', 'home'], check: ['all', 'rules-version', 'home'], resume: ['rules-version', 'delivery-id', 'home'], checkpoint: ['input', 'rules-version', 'revision', 'home'], adopt: ['input', 'rules-version', 'revision', 'home'], recover: ['rules-version', 'revision', 'home'], install: ['hosts', 'home', 'data-root'], uninstall: ['hosts', 'home'], help: [] };
129
+ const perCommand = { render: ['revision','rules-version','preserve-edits','home'], scan:['home'], status: ['home'], bind: ['home'], init: ['input', 'rules-version', 'home'], read: ['rules-version', 'delivery-id', 'home'], update: ['input', 'rules-version', 'revision', 'home'], check: ['all', 'rules-version', 'home'], resume: ['rules-version', 'delivery-id', 'home'], recover: ['rules-version', 'revision', 'home'], install: ['hosts', 'home', 'data-root'], uninstall: ['hosts', 'home'], help: [] };
125
130
  for (const key of Object.keys(options)) if (![...common, ...(perCommand[command] ?? [])].includes(key)) fail('USAGE', `Option --${key} is not valid for ${command}.`, `Run help ${command}.`);
126
131
  }
127
132
  export async function run(argv, context) {
@@ -150,16 +155,17 @@ export async function run(argv, context) {
150
155
  if (command === 'bind') return { command, data: await binding(runtime) };
151
156
  const directory = path.resolve(positional[1]); const rulesVersion = options['rules-version'];
152
157
  if (command === 'render') return {command,data:await renderState(directory,revisionOf(options.revision),rulesVersion,runtime,options['preserve-edits'])};
153
- if (command === 'migrate') return {command,data:await migrateState(directory,revisionOf(options.revision),rulesVersion,runtime,await inputJSON(options.input))};
154
- if (command === 'init') return { command, data: await initState(directory, await inputJSON(options.input), rulesVersion, runtime) };
155
- if (['update', 'checkpoint', 'adopt'].includes(command)) return { command, data: await updateState(directory, await inputJSON(options.input), revisionOf(options.revision), rulesVersion, runtime, command) };
158
+ if (command === 'init') {
159
+ const bound=await binding(runtime);
160
+ return { command, data: await initState(directory,await inputJSON(options.input),rulesVersion,{...runtime,dataRoot:bound.dataRoot}) };
161
+ }
162
+ if (command === 'update') return { command, data: await updateState(directory, await inputJSON(options.input), revisionOf(options.revision), rulesVersion, runtime) };
156
163
  if (command === 'recover') return { command, data: await recoverState(directory, revisionOf(options.revision), rulesVersion, runtime) };
157
164
  const read = await readState(directory);
158
165
  if (options['delivery-id'] && options['delivery-id'] !== read.state.deliveryId) fail('DELIVERY_ID_MISMATCH','This directory is not the bound delivery.','Locate the intended delivery; do not guess the latest one.');
159
166
  let mismatch = null;
160
- if (!read.readOnly) { try { assertRules(read.state, rulesVersion, runtime); } catch (error) { mismatch = { code: error.code, ...error.details, recovery: error.recovery }; } }
161
- if (command === 'read') return { command, data: { state: read.state, readOnly: read.readOnly, versionMismatch: mismatch, statePath: read.statePath, review:read.review, completion:read.completion, session:read.session, view: read.view, documentChanges: read.documentChanges, layoutConflict:read.conflict??false, layoutMismatch:read.layoutMismatch??false, candidates:read.candidates, migration:read.migration?.status??null } };
162
- if (read.readOnly) return { command, data: { state: read.state, readOnly: true, canProceed: false, reason: read.conflict?'Conflicting state layouts require explicit reconciliation.':'Unknown schema: no structure checking, mutation, or automatic work is permitted.', candidates:read.candidates } };
167
+ try { assertRules(read.state, rulesVersion, runtime); } catch (error) { mismatch = { code: error.code, ...error.details, recovery: error.recovery }; }
168
+ if (command === 'read') return { command, data: { state: read.state, readOnly: read.readOnly, versionMismatch: mismatch, statePath: read.statePath, review:read.review, completion:read.completion, session:read.session, view: read.view, documentChanges: read.documentChanges } };
163
169
  validateState(read.state); const references = await checkReferences(directory, read.state, options.all);
164
170
  const documents=await inspectDocuments(directory,read.state);
165
171
  const missing = references.filter(x => !['exists', 'not-checked'].includes(x.status));
@@ -171,5 +177,5 @@ export async function run(argv, context) {
171
177
  }
172
178
  const currentRun = read.state.stageRuns.find(x => x.id === read.state.currentRun) ?? null;
173
179
  const blocked = currentRun?.progress === 'blocked';
174
- return { command, data: { state: read.state, binding: await binding(runtime), readOnly: false, review:read.review, completion:read.completion, session:read.session, canProceed: read.review.canProceed && read.state.documentPolicy?.authority !== 'legacy' && !mismatch && missing.length === 0 && !blocked && !documents.issues.length && !read.documentChanges.length && read.view.status === 'current' && (!read.migration || read.migration.status === 'complete'), statePath:read.statePath, view:read.view, documentChanges:read.documentChanges, documents, versionMismatch: mismatch, references, currentRun, continuation: !mismatch && !missing.length && !documents.issues.length ? read.session?.continuation ?? null : null, warnings: ['Resume does not rerun work, tests, migrations, or other side effects.', 'Structural readiness does not establish authorization, business readiness, or goal completion.', ...(blocked ? [`Current run ${currentRun.id} is recorded as blocked. Resolve its documented blocker before progressing this work.`] : []), ...(missing.length ? ['Some required paths must be repaired.'] : []), ...(read.state.checkpoint ? ['Inspect the saved checkpoint and current artifacts.'] : [])] } };
180
+ return { command, data: { state: read.state, binding: await binding(runtime), readOnly: false, review:read.review, completion:read.completion, session:read.session, canProceed: read.review.canProceed && !mismatch && missing.length === 0 && !blocked && !documents.issues.length && !read.documentChanges.length && read.view.status === 'current', statePath:read.statePath, view:read.view, documentChanges:read.documentChanges, documents, versionMismatch: mismatch, references, currentRun, continuation: !mismatch && !missing.length && !documents.issues.length ? read.session?.continuation ?? null : null, warnings: [text(read.state,'Resume does not rerun work, tests, migrations, or other side effects.','续接不会重新执行工作、测试、迁移或其他外部操作。'), text(read.state,'Structural readiness does not establish authorization, business readiness, or goal completion.','结构检查就绪不代表已获得授权、业务已就绪或目标已完成。'), ...(blocked ? [text(read.state,`Current run ${currentRun.id} is recorded as blocked. Resolve its documented blocker before progressing this work.`,`当前工作 ${currentRun.id} 已记录为受阻,请先处理已记录的阻塞原因。`)] : []), ...(missing.length ? [text(read.state,'Some required paths must be repaired.','部分必要路径需要修复。')] : [])] } };
175
181
  }
package/lib/delivery.mjs CHANGED
@@ -1,11 +1,11 @@
1
+ import { text, completionMessage } from './language.mjs';
1
2
  import path from 'node:path';
2
3
  import { unknownConditions } from './documents.mjs';
3
4
  import { fail } from './io.mjs';
4
5
 
5
- // Additive policy: absent means historical, never inferred complete or rewritten on read.
6
+ // Current delivery policy and completion checks.
6
7
  export function validateDelivery(state) {
7
8
  const p = state.deliveryPolicy;
8
- if (p === undefined) return;
9
9
  if (!p || p.version !== 1) fail('INVALID_STATE', 'Unsupported delivery policy.', 'Preserve the state and use a compatible tool.');
10
10
  if (p.partialRef != null && (typeof p.partialRef !== 'string' || !p.partialRef.trim())) fail('INVALID_STATE', 'partialRef must identify the explicit scope decision.', 'Record the actual decision in the existing requirement or change record.');
11
11
  for (const r of state.stageRuns) {
@@ -24,9 +24,8 @@ export function arrangeSessions(state) {
24
24
  }
25
25
  }
26
26
  export function completionStatus(state, review, {changed=[]}={}) {
27
- if (state.deliveryPolicy?.version !== 1) return {status:'historical-unassessed', standardComplete:false, readyForAcceptance:false, gaps:['Historical outcomes are preserved; explicitly adopt the delivery policy after assessing evidence.']};
28
27
  const runs=state.stageRuns, gaps=[];
29
- if (state.schemaVersion === 3 && (state.goal.endpoint === null || state.documentPolicy.authority === null || unknownConditions(state.documentPolicy).length)) gaps.push('Endpoint or artifact conditions remain unknown.');
28
+ if (state.schemaVersion === 4 && (state.goal.endpoint === null || state.documentPolicy.authority === null || unknownConditions(state.documentPolicy).length)) gaps.push('Endpoint or artifact conditions remain unknown.');
30
29
  if(changed.length) gaps.push('Changed or missing registered documents require reconciliation.');
31
30
  const ready=r=>review.runs.find(x=>x.id===r.id)?.ready;
32
31
  const has=stage=>runs.some(r=>r.stage===stage && ready(r));
@@ -38,7 +37,7 @@ export function completionStatus(state, review, {changed=[]}={}) {
38
37
  const implementations=runs.filter(r=>r.stage==='implement');
39
38
  if (implementations.some(r=>['failed','mixed','unknown','not-run'].includes(r.result))) gaps.push('Implementation results remain unresolved.');
40
39
  const allReviews=runs.filter(r=>r.stage==='review');
41
- const effectiveReviews=allReviews.filter((r,i)=>!allReviews.slice(i+1).some(later=>ready(later) && later.result==='passed' && later.historyResultRefs?.includes(r.resultRef)));
40
+ const effectiveReviews=allReviews.filter(r=>!review.runs.find(x=>x.id===r.id)?.resolvedBy);
42
41
  if (effectiveReviews.some(r=>!ready(r) || r.result!=='passed')) gaps.push('Review results remain unresolved.');
43
42
  const reviews=allReviews.filter(r=>ready(r) && r.result==='passed');
44
43
  for (const [i, batch] of implementations.entries()) {
@@ -54,7 +53,7 @@ export function completionStatus(state, review, {changed=[]}={}) {
54
53
  if (state.openChanges.some(c=>c.status==='open')) gaps.push('Unresolved changes remain.');
55
54
  const partial=!!state.deliveryPolicy.partialRef;
56
55
  const readyForAcceptance=!partial && !gaps.length;
57
- return {status:partial?'partial-scope':readyForAcceptance && state.outcome.goal==='met' && state.outcome.acceptance==='accepted'?'standard-complete':readyForAcceptance?'ready-for-acceptance':'incomplete',standardComplete:readyForAcceptance && state.outcome.goal==='met' && state.outcome.acceptance==='accepted',readyForAcceptance,gaps,partialRef:state.deliveryPolicy.partialRef??null,scope:'Recorded structure, approvals and results only; not independent proof of test execution or semantic quality.'};
56
+ return {status:partial?'partial-scope':readyForAcceptance && state.outcome.goal==='met' && state.outcome.acceptance==='accepted'?'standard-complete':readyForAcceptance?'ready-for-acceptance':'incomplete',standardComplete:readyForAcceptance && state.outcome.goal==='met' && state.outcome.acceptance==='accepted',readyForAcceptance,gaps:gaps.map(g=>completionMessage(state,g)),partialRef:state.deliveryPolicy.partialRef??null,scope:text(state,'Recorded structure, approvals and results only; not independent proof of test execution or semantic quality.','仅评估已记录的结构、审批和结果;不独立证明测试已执行或内容语义正确。')};
58
57
  }
59
58
  export function sessionAdvice(directory,state,review,{saved=true}={}) {
60
59
  const groups=[];
@@ -65,14 +64,18 @@ export function sessionAdvice(directory,state,review,{saved=true}={}) {
65
64
  const status=review.runs.find(r=>r.id===state.currentRun);
66
65
  const pending=review.pending.some(p=>state.stageRuns.find(r=>r.id===p.run)?.progress==='done' || p.status==='changes-requested' || p.status==='self-check-failed');
67
66
  const next=current?state.stageRuns[state.stageRuns.indexOf(current)+1]:state.stageRuns.find(r=>r.progress!=='done');
67
+ const concluded=state.outcome.goal==='met' && review.runs.every(r=>r.ready)
68
+ && (!!state.deliveryPolicy?.partialRef || completionStatus(state,review).readyForAcceptance);
69
+ const last=state.stageRuns.at(-1);
70
+ const endAction=concluded?'finished':(current??last)?.stage==='handoff'?'assess-outcome':'arrange-next';
68
71
  let action='continue-current';
69
72
  if (!saved) action='reconcile-save';
70
- else if(state.deliveryPolicy?.version !== 1) action='assess-compatibility';
71
73
  else if(pending) action='await-review';
72
- else if(!current && state.stageRuns.length && review.runs.every(r=>r.ready)) action='finished';
73
- else if(current && (['failed','mixed','unknown','not-run'].includes(current.result) || current.progress==='blocked' || ['needs-review','unknown'].includes(current.applicability))) action='resolve-current';
74
- else if(status?.ready) action=!next?'finished':next.progress!=='planned'?'inspect-existing-work':current.sessionGroup && current.sessionGroup===next.sessionGroup?'continue-group':'suggest-new-session';
74
+ else if(!current && state.stageRuns.length && review.runs.every(r=>r.ready)) action=concluded?endAction:review.runs.some(r=>r.releaseReady===false)?'resolve-current':endAction;
75
+ else if(current && ((!status?.resolvedBy && (['failed','mixed','unknown','not-run'].includes(current.result) || (current.stage==='review' && current.progress==='done' && !status?.releaseReady))) || current.progress==='blocked' || ['needs-review','unknown'].includes(current.applicability))) action=concluded && !next?'finished':'resolve-current';
76
+ else if(status?.ready) action=!next?endAction:next.progress!=='planned'?'inspect-existing-work':current.sessionGroup && current.sessionGroup===next.sessionGroup?'continue-group':'suggest-new-session';
75
77
  if (saved && !pending && state.deliveryPolicy?.version === 1 && status?.ready && next && review.runs.find(r=>r.id===next.id)?.blockedBy.length) action='resolve-current';
76
- const continuation=['assess-compatibility','reconcile-save','await-review','finished'].includes(action)?null:`Use Aimship to continue delivery ${JSON.stringify(path.resolve(directory))}; first read its current state and relevant formal documents, then continue the recorded work.`;
77
- return {action,currentRun:current?.id??null,nextRun:next?.id??null,groups,continuation,advisory:true,note:'A session boundary is not a blocker or approval. Continue here when requested. Resolve the delivery identity, current approvals and unfinished effects on every resume; this text grants no approval.'};
78
+ const instruction=action==='arrange-next'?text(state,'The recorded work has ended, but the delivery goal remains open. Read the agreed endpoint and saved next action; arrange only the next authorized work, or record an evidenced scope conclusion.','已登记工作已完成,但交付目标尚未结束。请读取约定终点与已保存的下一步,安排下一项已授权工作,或依据实际证据记录范围结论。'):action==='assess-outcome'?text(state,'Review the handoff against the agreed goal and record the actual goal conclusion and acceptance; completed handoff alone does not end the delivery.','请对照约定目标审阅交付材料,记录实际目标结论和用户接受结果;完成交付文档不等于交付已结束。'):null;
79
+ const continuation=['reconcile-save','await-review','assess-outcome','finished'].includes(action)?null:text(state,`Use Aimship to continue delivery ${JSON.stringify(path.resolve(directory))}; first read its current state and relevant formal documents, then continue the recorded work.${instruction?' '+instruction:''}`,`使用 Aimship 继续交付「${path.resolve(directory)}」,先读取当前状态和相关正式文档再接续。${instruction??''}`);
80
+ return {action,currentRun:current?.id??null,nextRun:next?.id??null,groups,continuation,instruction,advisory:true,note:text(state,'A session boundary is not a blocker or approval. Continue here when requested. Resolve the delivery identity, current approvals and unfinished effects on every resume; this text grants no approval.','会话边界不是阻塞或批准。用户要求时可在当前会话继续。每次续接都需核对交付身份、当前审批和未完成事项;本提示不授予权限。')};
78
81
  }
@@ -0,0 +1,25 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { exists, noSymlinks } from './io.mjs';
4
+ import { readState } from './state.mjs';
5
+
6
+
7
+ // Discovery is intentionally bounded to named material roots, never the home or code tree.
8
+ export async function scanDeliveries(root) {
9
+ const records=[],issues=[],seen=new Map();
10
+ for(const entry of await fs.readdir(root,{withFileTypes:true})) {
11
+ if(entry.name.startsWith('.')) continue;
12
+ const directory=path.join(root,entry.name);
13
+ if(entry.isSymbolicLink()) {issues.push({directory,reason:'SYMLINK'}); continue;}
14
+ if(!entry.isDirectory()) continue;
15
+ try {
16
+ await noSymlinks(path.join(directory,'.aimship/state.json'));
17
+ if(!await exists(path.join(directory,'.aimship/state.json'))) continue;
18
+ const read=await readState(directory), id=read.state.deliveryId;
19
+ if(seen.has(id)) {issues.push({directory,reason:'duplicate-delivery-id',other:seen.get(id)}); continue;}
20
+ seen.set(id,directory);
21
+ records.push({directory,statePath:read.statePath,deliveryId:id,rulesVersion:read.state.rulesVersion,repositories:read.state.repositories,currentRun:read.state.currentRun,stageRuns:read.state.stageRuns,outcome:read.state.outcome,next:read.state.next,updatedAt:read.state.updatedAt});
22
+ } catch(error) {issues.push({directory,reason:error.code??error.message});}
23
+ }
24
+ return {root:path.resolve(root),records,issues,limits:'Named root immediate children only. Records may be stale; unknown roots and non-Aimship work are outside this check. Agent must assess actual writing intent and checkout/worktree identity.'};
25
+ }