frontend-project-context 1.8.0 → 1.10.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 (69) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +26 -7
  3. package/UPGRADING.md +24 -0
  4. package/docs/00-PRODUCT-CONSTITUTION.md +44 -12
  5. package/docs/08-INSTALLATION-AND-DISTRIBUTION.md +9 -3
  6. package/docs/14-FORMAL-RELEASE-READINESS.md +14 -0
  7. package/docs/28-REAL-PROJECT-ONBOARDING-CLOSURE-DESIGN.md +10 -0
  8. package/docs/29-REAL-PROJECT-1.8.0-INITIALIZATION-OBSERVATIONS.md +228 -0
  9. package/docs/30-TASK-CONTEXT-CONSUMPTION-CLOSURE-DESIGN.md +563 -0
  10. package/docs/31-TASK-CONTEXT-INTEGRITY-REPAIR-DESIGN.md +281 -0
  11. package/docs/32-PROJECT-TO-TASK-INTERACTION-PROPOSAL.md +173 -0
  12. package/docs/33-BOUNDED-TASK-HANDOFF-REDESIGN.md +207 -0
  13. package/docs/34-A0-CODEX-HOST-FEASIBILITY.md +61 -0
  14. package/docs/35-CONTEXT-FIRST-TASK-HANDOFF-DESIGN.md +248 -0
  15. package/docs/36-PRODUCT-VALUE-AND-USABILITY-REVIEW.md +158 -0
  16. package/docs/37-EVERYDAY-HANDOFF-FLOW-DESIGN.md +271 -0
  17. package/docs/38-EVERYDAY-HANDOFF-OPERATION.md +147 -0
  18. package/docs/39-SELF-HOST-RELEASE-ACCEPTANCE.md +39 -0
  19. package/docs/AI-PROJECT-INITIALIZATION.md +8 -3
  20. package/docs/PRODUCT-SHARING-AND-ADOPTION-GUIDE.md +111 -0
  21. package/docs/README.md +26 -2
  22. package/docs/USER-AND-AI-OPERATION-MANUAL.md +9 -3
  23. package/docs/assets/product-sharing-01-overview.svg +32 -0
  24. package/docs/assets/product-sharing-02-how-it-works.svg +17 -0
  25. package/docs/assets/product-sharing-03-example.svg +19 -0
  26. package/examples/package.json +1 -1
  27. package/migration-manifest.json +38 -12
  28. package/package.json +2 -2
  29. package/schemas/capabilities.schema.json +26 -12
  30. package/schemas/context-bundle.schema.json +32 -0
  31. package/schemas/coverage-audit.schema.json +6 -4
  32. package/schemas/evidence-bundle.schema.json +1 -1
  33. package/schemas/initialization-instruction.schema.json +17 -5
  34. package/schemas/migration-manifest.schema.json +3 -3
  35. package/schemas/migration-plan.schema.json +1 -1
  36. package/schemas/project-brief.schema.json +400 -0
  37. package/schemas/projection-lock.schema.json +1 -1
  38. package/schemas/task-context-record.schema.json +27 -0
  39. package/schemas/task-handoff-input.schema.json +23 -0
  40. package/schemas/task-handoff-result.schema.json +320 -0
  41. package/schemas/upgrade-assessment.schema.json +1 -1
  42. package/schemas/upgrade-result-bundle.schema.json +1 -1
  43. package/schemas/work-view.schema.json +541 -0
  44. package/src/project-context/adaptive-context-schema.mjs +1 -1
  45. package/src/project-context/adaptive-context.mjs +16 -29
  46. package/src/project-context/ai-entry.mjs +28 -12
  47. package/src/project-context/approver.mjs +6 -2
  48. package/src/project-context/authoring.mjs +28 -8
  49. package/src/project-context/capabilities.mjs +13 -1
  50. package/src/project-context/checker.mjs +1 -1
  51. package/src/project-context/cli.mjs +74 -17
  52. package/src/project-context/context-bundle.mjs +379 -0
  53. package/src/project-context/contract-schema.mjs +62 -9
  54. package/src/project-context/coverage-profile.mjs +127 -0
  55. package/src/project-context/exchange-schema.mjs +24 -6
  56. package/src/project-context/exchange.mjs +3 -0
  57. package/src/project-context/initialization-instruction.mjs +20 -2
  58. package/src/project-context/maintenance.mjs +5 -4
  59. package/src/project-context/migration-manifest.mjs +3 -3
  60. package/src/project-context/path-policy.mjs +13 -0
  61. package/src/project-context/renderer.mjs +11 -4
  62. package/src/project-context/source-reader.mjs +27 -2
  63. package/src/project-context/task-context.mjs +4 -14
  64. package/src/project-context/task-handoff-schema.mjs +185 -0
  65. package/src/project-context/task-handoff.mjs +259 -0
  66. package/src/project-context/upgrade-schema.mjs +1 -1
  67. package/src/project-context/upgrade.mjs +1 -0
  68. package/src/project-context/work-view-schema.mjs +125 -0
  69. package/src/project-context/work-view.mjs +135 -0
@@ -6,25 +6,29 @@ import { fail } from "./errors.mjs";
6
6
  import { atomicWriteFile, atomicWriteJson, readJsonFile } from "./io.mjs";
7
7
  import { normalizeRelativePath, resolveWritableInside } from "./path-policy.mjs";
8
8
 
9
- export const AI_ENTRY_RENDERER_VERSION = 4;
9
+ export const AI_ENTRY_RENDERER_VERSION = 7;
10
10
  export const AI_ENTRY_REGION_ID = "project-context-ai-entry";
11
11
  export const AI_ENTRY_START = "<!-- project-context:ai-entry:start -->";
12
12
  export const AI_ENTRY_END = "<!-- project-context:ai-entry:end -->";
13
13
 
14
14
  const ENTRY_LINES = [
15
15
  AI_ENTRY_START,
16
- "<!-- project-context:ai-entry; schema-version: 1; renderer-version: 4 -->",
16
+ "<!-- project-context:ai-entry; schema-version: 1; renderer-version: 7 -->",
17
17
  "## Project Context 启动流程",
18
18
  "",
19
19
  "开始项目工作前,使用项目本地安装的 `frontend-project-context` CLI,不要临时下载其他版本。",
20
20
  "",
21
- "1. 运行 `npm exec --offline -- project-context status --project . --json`;如果本地依赖不存在,停止并报告,不要临时下载同名包。",
22
- "2. 如果状态为 `uninitialized`,运行 `npm exec --offline -- project-context instructions --project . --prompt`,按包内唯一指令完成完整初始化;如果为 `partial` 或 `invalid`,停止写入并报告准确的恢复证据。",
23
- "3. 如果状态为 `attention`,执行 `sync` 返回的维护工作单元;不得重新运行 `setup` 覆盖已有 store。",
24
- "4. 如果状态为 `clean`,根据真实任务确定项目内目标路径,并运行 `npm exec --offline -- project-context context --project . --path <RELATIVE_PATH> --task <TEXT>`。",
25
- "5. 必须先消费命中的 Project Contract,报告实际 item IDs、scope 和必要 read targets;Contract 为空、目标 scope 未命中或关键上下文不足时,报告 onboarding/context gap 并停止,不能直接进入源码。",
26
- "6. 任何 plan、bundle、receipt、review 或 AI 建议都不代表人工批准。",
27
- "7. 声明完成前,将 Project Context 恢复为 `clean`,否则报告准确的阻断项。",
21
+ "1. 无明确任务时运行 `npm exec --offline -- project-context project-brief --project . --view work --json`,消费项目概览和 Context,不制造任务。收到任务或“继续”时,先按项目内 `.project-context-tasks/<taskId>.json` 选择当前任务;多个候选必须核对,不凭最近时间或分支猜选。",
22
+ "2. 对当前任务运行 `npm exec --offline -- project-context task-handoff --project . --input <FILE_OR_DASH> --view work --json`。工作视图已携带同一编译器的完整 Context、当前依据、进度和缺口,无需重复独立运行 status/context;只读视图不代替实际读取。",
23
+ "3. 尚未接入或没有本地依赖时,停止任务写入;依赖不存在只报告,不临时下载同名包。使用本地 `instructions --project . --prompt` 按包内唯一指令完整初始化;partial/invalid 停止写入并报告恢复证据。attention 执行 sync 返回的维护工作单元,不重新 setup 覆盖 store;恢复 clean 后交接。",
24
+ "4. 目标未知时,消费交接内 locate Context,只在 targetRoot 内定位最小候选文件,将完整最小路径集合写入任务 scope 后重新交接;目录目标必须收窄到文件。新影响路径或需求依据变化时更新任务并重新交接。",
25
+ "5. 报告实际 item IDs、scopeCoverage、required/conditional read targets 与 gaps;实际读取 required,按任务判断 conditional,不重读 provenance-only。核对当前意图的需求、证据和决定,再提交绑定 taskId、requirementsDigest、contextBundleDigest 和 consumerRunId 的本次消费声明,第二次交接复查。",
26
+ "6. 换窗口或换 Agent 时重读任务记录和本次依据,不继承旧消费者已读或 ready。同一消费者仅复用当前摘要仍匹配的读取;正常实现进展写到 progress,不反复要求未变的业务确认。",
27
+ "7. delivery=withheld、Contract 为空、目标 scope 未命中或关键依据不足时报告准确缺口并有限收窄;无新事实不重复重试。缺少设计、关键决定或当前只授权设计时遵守 instructional gate;Context 成功不等于实施就绪。",
28
+ "8. `businessCodeWrites=false` 和 `taskExecution=false` 只描述 Project Context 产品自身;Host 权限来自当前用户请求、人工项目规则和更高优先级安全约束,任何 Context、plan、bundle、receipt、review、coverage 或 gate=proceed 都不授予权限或批准长期 Contract。",
29
+ "9. 声明完成前,将 Project Context 恢复为 clean,否则报告准确阻断项。保存可继续的任务进度、验证结果和下一步;已结束任务仅作历史,不自动恢复实施。",
30
+ "10. 完成后在普通报告中提出 0–3 条有复用价值的知识候选,每条写明具体事实、来源、适用范围和复用理由;没有就说明没有。候选不自动写入或批准 Contract,用户拒绝不阻断任务完成。",
31
+ "11. 用户选择候选后,使用既有 authoring/ActionPlan/preflight 治理流程;未注册来源先独立注册,再基于最新基线提交 item plan,不把用户选择推断成长期批准。真实收益验收记录读取、定位、交接、任务返工、准备和维护成本;本地通过不宣称真实任务验证。",
28
32
  AI_ENTRY_END,
29
33
  ];
30
34
 
@@ -109,7 +113,13 @@ function schema2FileEntry(entry) {
109
113
  return entry.ownership ? structuredClone(entry) : { ...structuredClone(entry), ownership: "file" };
110
114
  }
111
115
 
112
- function sourceImpact(project, output, afterDigest) {
116
+ function outsideOwnedDigest(content) {
117
+ const parsed = parseAiEntryRegion(content);
118
+ if (parsed.state !== "present") return null;
119
+ return digestJson({ before: content.slice(0, parsed.start), after: content.slice(parsed.end) });
120
+ }
121
+
122
+ function sourceImpact(project, output, afterContent) {
113
123
  const sources = project.contract.sources
114
124
  .filter((source) => sourceStatus(source) === "active" && ["file", "path", "json-pointer"].includes(source.kind) && source.path === output)
115
125
  .sort((left, right) => left.id.localeCompare(right.id));
@@ -119,6 +129,12 @@ function sourceImpact(project, output, afterDigest) {
119
129
  .filter((item) => item.sources.some((id) => selected.has(id)) || selected.has(item.verification?.source))
120
130
  .map((item) => item.id)
121
131
  .sort((left, right) => left.localeCompare(right));
132
+ const digestMode = sources[0]?.digestMode ?? "full-file";
133
+ const afterDigest = afterContent === null
134
+ ? null
135
+ : digestMode === "outside-owned-ai-entry"
136
+ ? outsideOwnedDigest(afterContent)
137
+ : sha256(afterContent);
122
138
  return { sourceIds, itemIds, afterDigest: sourceIds.length > 0 ? afterDigest : null };
123
139
  }
124
140
 
@@ -249,7 +265,7 @@ export async function publishAiEntry(root, project, options, dependencies = {})
249
265
  };
250
266
  const nextLock = nextProjectionLock(project, resolved.normalized, nextEntry);
251
267
  const action = currentEntry && existing === nextContent && canonicalJson(currentEntry) === canonicalJson(nextEntry) ? "unchanged" : currentEntry ? "update" : "create";
252
- const impact = sourceImpact(project, resolved.normalized, sha256(nextContent));
268
+ const impact = sourceImpact(project, resolved.normalized, nextContent);
253
269
  const result = {
254
270
  action,
255
271
  current: {
@@ -304,7 +320,7 @@ export async function removeAiEntry(root, project, options, dependencies = {}) {
304
320
  }
305
321
  const nextContent = `${existing.slice(0, parsed.start)}${existing.slice(parsed.end)}`;
306
322
  const nextLock = nextProjectionLock(project, resolved.normalized, null);
307
- const impact = sourceImpact(project, resolved.normalized, sha256(nextContent));
323
+ const impact = sourceImpact(project, resolved.normalized, nextContent);
308
324
  const result = {
309
325
  action: "remove",
310
326
  current: { entry: structuredClone(currentEntry), contentDigest: sha256(existing), region: parsed.region },
@@ -1,5 +1,6 @@
1
1
  import { canonicalJson } from "./canonical-json.mjs";
2
2
  import {
3
+ promoteContractToSchema3,
3
4
  sourceForContract,
4
5
  sourceRegistrationShape,
5
6
  sourceStatus,
@@ -52,7 +53,7 @@ export async function approveProposal(root, project, proposalInput, options) {
52
53
  for (const sourceId of selectedSourceIds) {
53
54
  const source = proposalSources.get(sourceId);
54
55
  if (!source) fail("proposal-source-missing", `proposal source is missing: ${sourceId}`);
55
- const actual = await readSourceDigest(root, source);
56
+ const actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
56
57
  if (actual !== null && actual !== source.digest) {
57
58
  fail("proposal-source-changed", `source changed after discovery: ${sourceId}`, {
58
59
  exitCode: 1,
@@ -61,6 +62,9 @@ export async function approveProposal(root, project, proposalInput, options) {
61
62
  }
62
63
  }
63
64
  const nextContract = structuredClone(project.contract);
65
+ if (selected.some((item) => item.consumption !== undefined) || [...selectedSourceIds].some((id) => proposalSources.get(id)?.digestMode !== undefined)) {
66
+ promoteContractToSchema3(nextContract);
67
+ }
64
68
  for (const sourceId of selectedSourceIds) {
65
69
  const source = proposalSources.get(sourceId);
66
70
  const existing = nextContract.sources.find((entry) => entry.id === sourceId);
@@ -141,7 +145,7 @@ export async function approvePendingItems(root, project, options) {
141
145
  }
142
146
  let actual;
143
147
  try {
144
- actual = await readSourceDigest(root, source);
148
+ actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
145
149
  } catch (error) {
146
150
  fail("pending-source-changed", `pending item source is unavailable: ${sourceId}`, {
147
151
  exitCode: 1,
@@ -1,5 +1,6 @@
1
1
  import { canonicalJson } from "./canonical-json.mjs";
2
2
  import {
3
+ promoteContractToSchema3,
3
4
  sourceForContract,
4
5
  sourceRegistrationShape,
5
6
  sourceStatus,
@@ -32,8 +33,8 @@ function sourceLocator(source) {
32
33
  });
33
34
  }
34
35
 
35
- export async function buildRegisteredSource(root, input) {
36
- const { id, kind, path: sourcePath, pointer, reference } = input;
36
+ export async function buildRegisteredSource(root, input, options = {}) {
37
+ const { id, kind, path: sourcePath, pointer, reference, digestMode } = input;
37
38
  let source;
38
39
  if (LOCAL_SOURCE_KINDS.has(kind)) {
39
40
  if (!has(sourcePath) || has(reference) || (kind === "json-pointer" ? !has(pointer) : has(pointer))) {
@@ -44,21 +45,23 @@ export async function buildRegisteredSource(root, input) {
44
45
  kind,
45
46
  path: normalizeRelativePath(sourcePath, { label: "source path" }),
46
47
  ...(kind === "json-pointer" ? { pointer } : {}),
48
+ ...(digestMode !== undefined ? { digestMode } : {}),
47
49
  };
48
- source.digest = await readSourceDigest(root, source);
50
+ if (digestMode !== undefined && kind !== "file") fail("argument-conflict", "--digest-mode is only valid for file sources");
51
+ source.digest = await readSourceDigest(root, source, undefined, { projectionsLock: options.projectionsLock });
49
52
  } else if (REFERENCE_SOURCE_KINDS.has(kind)) {
50
- if (!has(reference) || has(sourcePath) || has(pointer)) {
53
+ if (!has(reference) || has(sourcePath) || has(pointer) || has(digestMode)) {
51
54
  fail("argument-conflict", `${kind} source requires only --reference`, { details: { source: id } });
52
55
  }
53
56
  source = { id, kind, reference };
54
57
  } else {
55
58
  fail("schema-invalid-enum", `source kind is invalid: ${kind}`, { details: { source: id } });
56
59
  }
57
- return validateSource(source);
60
+ return validateSource(source, "source", { contractSchemaVersion: digestMode === undefined ? 1 : 3, proposal: true });
58
61
  }
59
62
 
60
63
  export async function registerSource(root, project, input, options = {}) {
61
- const source = await buildRegisteredSource(root, input);
64
+ const source = await buildRegisteredSource(root, input, { projectionsLock: project.projectionsLock });
62
65
  const existingById = project.contract.sources.find((entry) => entry.id === source.id);
63
66
  if (existingById) {
64
67
  if (sourceStatus(existingById) === "deprecated" || canonicalJson(sourceRegistrationShape(existingById)) !== canonicalJson(source)) {
@@ -77,6 +80,7 @@ export async function registerSource(root, project, input, options = {}) {
77
80
  }
78
81
 
79
82
  const nextContract = structuredClone(project.contract);
83
+ if (source.digestMode !== undefined) promoteContractToSchema3(nextContract);
80
84
  const storedSource = sourceForContract(source, nextContract.schemaVersion);
81
85
  nextContract.sources.push(storedSource);
82
86
  nextContract.sources.sort((left, right) => left.id.localeCompare(right.id));
@@ -91,7 +95,7 @@ export async function registerSource(root, project, input, options = {}) {
91
95
 
92
96
  if (options.write) {
93
97
  if (LOCAL_SOURCE_KINDS.has(source.kind)) {
94
- const actual = await readSourceDigest(root, source);
98
+ const actual = await readSourceDigest(root, source, undefined, { projectionsLock: project.projectionsLock });
95
99
  if (actual !== source.digest) {
96
100
  fail("source-changed-during-register", `source changed while registration was being prepared: ${source.id}`, {
97
101
  exitCode: 1,
@@ -157,6 +161,21 @@ export function buildItemProposal(project, input) {
157
161
  details: { item: input.id, source: verification.source },
158
162
  });
159
163
  }
164
+ const requiredSources = input.requiredSources ?? [];
165
+ const conditionalSources = input.conditionalSources ?? [];
166
+ if (new Set(requiredSources).size !== requiredSources.length) fail("schema-duplicate", "--required-source contains duplicate IDs");
167
+ if (new Set(conditionalSources).size !== conditionalSources.length) fail("schema-duplicate", "--conditional-source contains duplicate IDs");
168
+ const roleSources = [...requiredSources, ...conditionalSources];
169
+ const overlap = requiredSources.find((source) => conditionalSources.includes(source));
170
+ if (overlap) fail("argument-conflict", `source cannot be both required and conditional: ${overlap}`);
171
+ for (const sourceId of roleSources) {
172
+ if (!sourceIds.includes(sourceId)) fail("source-reference-missing", `consumption source must be included in --sources: ${sourceId}`);
173
+ if (!LOCAL_SOURCE_KINDS.has(sourceMap.get(sourceId)?.kind)) fail("argument-conflict", `consumption source must be local: ${sourceId}`);
174
+ }
175
+ const consumption = roleSources.length > 0 ? {
176
+ requiredSources: [...requiredSources].sort((left, right) => left.localeCompare(right)),
177
+ conditionalSources: [...conditionalSources].sort((left, right) => left.localeCompare(right)),
178
+ } : undefined;
160
179
  const item = {
161
180
  id: input.id,
162
181
  kind: input.kind,
@@ -167,9 +186,10 @@ export function buildItemProposal(project, input) {
167
186
  status: "proposed",
168
187
  sources: sourceIds,
169
188
  overrides: input.overrides ?? [],
189
+ ...(consumption ? { consumption } : {}),
170
190
  ...(verification ? { verification } : {}),
171
191
  };
172
- validateItem(item, "proposal item", { proposal: true });
192
+ validateItem(item, "proposal item", { proposal: true, contractSchemaVersion: 3 });
173
193
 
174
194
  if (item.overrides.length > 0) {
175
195
  const hypothetical = { ...structuredClone(item), status: "approved", approval: { by: "proposal-preflight", at: "1970-01-01T00:00:00.000Z" } };
@@ -13,6 +13,8 @@ import { inspectProjectInitialization, loadProject } from "./project-store.mjs";
13
13
  import { RENDERER_VERSION } from "./renderer.mjs";
14
14
  import { AI_ENTRY_RENDERER_VERSION } from "./ai-entry.mjs";
15
15
  import { PROJECT_STATUS_SCHEMA_VERSION } from "./project-status.mjs";
16
+ import { CONTEXT_BUNDLE_SCHEMA_VERSION } from "./context-bundle.mjs";
17
+ import { TASK_RECORD_SCHEMA_VERSION, TASK_HANDOFF_INPUT_SCHEMA_VERSION, TASK_HANDOFF_RESULT_SCHEMA_VERSION } from "./task-handoff-schema.mjs";
16
18
  import { EVIDENCE_BUNDLE_SCHEMA_VERSION, EVIDENCE_INPUT_SCHEMA_VERSION } from "./evidence-schema.mjs";
17
19
  import {
18
20
  CONTEXT_BUDGET_UNIT,
@@ -49,7 +51,13 @@ function schemas(projectionLockWritten = 1) {
49
51
  adaptiveContextBundle: ADAPTIVE_CONTEXT_BUNDLE_SCHEMA_VERSION,
50
52
  assistBundle: ASSIST_BUNDLE_SCHEMA_VERSION,
51
53
  capabilities: CAPABILITIES_SCHEMA_VERSION,
52
- contract: 2,
54
+ contract: 3,
55
+ contractReadable: [1, 2, 3],
56
+ contextBundle: CONTEXT_BUNDLE_SCHEMA_VERSION,
57
+ taskContextRecord: TASK_RECORD_SCHEMA_VERSION,
58
+ taskHandoffInput: TASK_HANDOFF_INPUT_SCHEMA_VERSION,
59
+ taskHandoffResult: TASK_HANDOFF_RESULT_SCHEMA_VERSION,
60
+ projectBrief: 1,
53
61
  contextQuery: CONTEXT_QUERY_SCHEMA_VERSION,
54
62
  coverageAudit: COVERAGE_AUDIT_SCHEMA_VERSION,
55
63
  dashboardViewModel: DASHBOARD_SCHEMA_VERSION,
@@ -115,6 +123,10 @@ export async function buildCapabilities(root) {
115
123
  package: { name: "frontend-project-context", version: PACKAGE_VERSION },
116
124
  exchangeProtocolVersion: EXCHANGE_PROTOCOL_VERSION,
117
125
  schemas: schemas(projectionLockWritten),
126
+ views: {
127
+ projectBrief: { default: "legacy", legacy: { schemaVersion: 1 }, work: { schemaVersion: 2 } },
128
+ taskHandoff: { default: "legacy", legacy: { schemaVersion: 1 }, work: { schemaVersion: 2 } },
129
+ },
118
130
  commands: [...COMMANDS],
119
131
  actionKinds: [...ACTION_KINDS],
120
132
  contextBudget: { unit: CONTEXT_BUDGET_UNIT, modelTokens: false, callerMustProvideLimit: true },
@@ -26,7 +26,7 @@ export async function checkProject(root, project, options = {}) {
26
26
  continue;
27
27
  }
28
28
  try {
29
- const actual = await readSourceDigest(root, source, sourceReadContext);
29
+ const actual = await readSourceDigest(root, source, sourceReadContext, { projectionsLock: project.projectionsLock });
30
30
  if (actual !== locked) findings.push({ code: "source-changed", source: source.id, path: source.path, expected: locked, actual });
31
31
  } catch (error) {
32
32
  const code = error.code === "source-missing"
@@ -5,6 +5,7 @@ import { buildItemProposal, registerSource } from "./authoring.mjs";
5
5
  import { blockingContextFindings, checkExitCode, checkProject } from "./checker.mjs";
6
6
  import { digestJson, prettyCanonicalJson } from "./canonical-json.mjs";
7
7
  import { validateProposal } from "./contract-schema.mjs";
8
+ import { buildContextBundle } from "./context-bundle.mjs";
8
9
  import { buildDashboardModel } from "./dashboard-model.mjs";
9
10
  import { renderDashboardHtml } from "./dashboard-renderer.mjs";
10
11
  import { discoverProject } from "./discovery.mjs";
@@ -20,10 +21,11 @@ import { initializeProject, inspectProjectInitialization, loadProject } from "./
20
21
  import { publishProjection } from "./projection-store.mjs";
21
22
  import { publishAiEntry, removeAiEntry } from "./ai-entry.mjs";
22
23
  import { buildProjectStatus } from "./project-status.mjs";
23
- import { renderContextBundle } from "./renderer.mjs";
24
24
  import { buildIntegrationReviewBundleFiles, buildStageContextBundleFiles } from "./task-context.mjs";
25
25
  import { applyMigrationPlanFile, buildMigrationPlanFile, buildUpgradeAssessment } from "./upgrade.mjs";
26
26
  import { buildTruthReconciliationReviewFiles } from "./truth-reconciliation.mjs";
27
+ import { buildProjectBrief, buildTaskHandoff } from "./task-handoff.mjs";
28
+ import { renderWorkView } from "./work-view.mjs";
27
29
  import { buildInitializationInstruction, renderInitializationPrompt } from "./initialization-instruction.mjs";
28
30
 
29
31
  const HELP = `project-context — model-neutral project contract compiler
@@ -33,21 +35,23 @@ Usage:
33
35
  project-context capabilities --project PATH [--json]
34
36
  project-context instructions --project PATH [--json | --prompt]
35
37
  project-context status --project PATH [--json]
38
+ project-context project-brief --project PATH [--view legacy|work] [--json]
39
+ project-context task-handoff --project PATH --input FILE_OR_DASH [--view legacy|work] [--json]
36
40
  project-context evidence --project PATH --input FILE [--json]
37
41
  project-context upgrade-check --project PATH --from-version VERSION [--json]
38
42
  project-context upgrade-plan --project PATH --assessment FILE [--json]
39
43
  project-context upgrade-apply --project PATH --plan FILE [--write] [--json]
40
44
  project-context setup --project PATH --id ID --name NAME [--output FILE] [--write] [--json]
41
- project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--write] [--json]
42
- project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
45
+ project-context register --project PATH --id SOURCE_ID --kind KIND [--path PATH] [--pointer POINTER] [--reference TEXT] [--digest-mode full-file|outside-owned-ai-entry] [--write] [--json]
46
+ project-context propose --project PATH --id ITEM_ID --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--output FILE --write] [--json]
43
47
  project-context review-source --project PATH --id SOURCE_ID [--json]
44
48
  project-context accept-source-change --project PATH --id SOURCE_ID --expected-digest SHA256 [--affected-items ITEM_ID...] [--write] [--json]
45
- project-context revise --project PATH --id ITEM_ID [--expected-item-digest SHA256] --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--write] [--json]
49
+ project-context revise --project PATH --id ITEM_ID [--expected-item-digest SHA256] --kind KIND --subject SUBJECT (--value TEXT | --value-json JSON) --statement TEXT --sources SOURCE_ID... --scope SCOPE [--scope-path PATH] [--overrides ITEM_ID...] [--required-source SOURCE_ID...] [--conditional-source SOURCE_ID...] [--verification KIND] [--verification-source SOURCE_ID] [--verification-expected-json JSON] [--write] [--json]
46
50
  project-context deprecate --project PATH --id ITEM_ID [--expected-item-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
47
51
  project-context deprecate-source --project PATH --id SOURCE_ID [--expected-source-digest SHA256] --by NAME --rationale TEXT [--write] [--json]
48
52
  project-context discover --project PATH [--output FILE --write] [--json]
49
53
  project-context approve --project PATH (--proposal FILE | --pending) --ids ID... --by NAME [--rationale TEXT] [--write] [--json | --full-json]
50
- project-context context --project PATH --path RELATIVE_PATH... [--task TEXT] [--locale zh-CN|en|all] [--json]
54
+ project-context context --project PATH (--locate --task TEXT | --path RELATIVE_PATH... [--task TEXT]) [--locale zh-CN|en|all] [--json]
51
55
  project-context context-query --project PATH --input FILE [--previous FILE] [--json | --prompt]
52
56
  project-context coverage-audit --project PATH [--changed-path RELATIVE_PATH...] [--json]
53
57
  project-context index-context --project PATH [--write] [--json]
@@ -67,27 +71,29 @@ All commands are read-only unless their own --write flag is present.
67
71
  const VALUE_FLAGS = new Set([
68
72
  "project", "id", "name", "output", "proposal", "by", "task", "target", "rationale",
69
73
  "kind", "pointer", "reference", "subject", "value", "value-json", "statement", "scope", "scope-path",
70
- "verification", "verification-source", "verification-expected-json",
71
- "expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment", "previous", "previous-review",
74
+ "verification", "verification-source", "verification-expected-json", "digest-mode",
75
+ "expected-digest", "expected-item-digest", "expected-source-digest", "locale", "plan", "stage", "from-version", "assessment", "previous", "previous-review", "view",
72
76
  ]);
73
77
  const LIST_FLAGS = new Set([
74
- "ids", "path", "input", "changed-path", "sources", "overrides", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
78
+ "ids", "path", "input", "changed-path", "sources", "overrides", "required-source", "conditional-source", "affected-items", "receipt", "receipt-bundle", "main-changed-path", "branch-changed-path",
75
79
  ]);
76
- const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt"]);
80
+ const BOOLEAN_FLAGS = new Set(["write", "json", "full-json", "help", "pending", "prompt", "locate"]);
77
81
  const COMMAND_OPTIONS = new Map([
78
82
  ["init", new Set(["project", "id", "name", "write", "json", "help"])],
79
83
  ["capabilities", new Set(["project", "json", "help"])],
80
84
  ["instructions", new Set(["project", "json", "prompt", "help"])],
81
85
  ["status", new Set(["project", "json", "help"])],
86
+ ["project-brief", new Set(["project", "json", "help", "view"])],
87
+ ["task-handoff", new Set(["project", "input", "json", "help", "view"])],
82
88
  ["evidence", new Set(["project", "input", "json", "help"])],
83
89
  ["upgrade-check", new Set(["project", "from-version", "json", "help"])],
84
90
  ["upgrade-plan", new Set(["project", "assessment", "json", "help"])],
85
91
  ["upgrade-apply", new Set(["project", "plan", "write", "json", "help"])],
86
92
  ["setup", new Set(["project", "id", "name", "output", "write", "json", "help"])],
87
- ["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "write", "json", "help"])],
93
+ ["register", new Set(["project", "id", "kind", "path", "pointer", "reference", "digest-mode", "write", "json", "help"])],
88
94
  ["propose", new Set([
89
95
  "project", "id", "kind", "subject", "value", "value-json", "statement", "sources", "scope", "scope-path",
90
- "overrides", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
96
+ "overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json", "output", "write", "json", "help",
91
97
  ])],
92
98
  ["review-source", new Set(["project", "id", "json", "help"])],
93
99
  ["accept-source-change", new Set([
@@ -95,7 +101,7 @@ const COMMAND_OPTIONS = new Map([
95
101
  ])],
96
102
  ["revise", new Set([
97
103
  "project", "id", "expected-item-digest", "kind", "subject", "value", "value-json", "statement", "sources",
98
- "scope", "scope-path", "overrides", "verification", "verification-source", "verification-expected-json",
104
+ "scope", "scope-path", "overrides", "required-source", "conditional-source", "verification", "verification-source", "verification-expected-json",
99
105
  "write", "json", "help",
100
106
  ])],
101
107
  ["deprecate", new Set([
@@ -106,7 +112,7 @@ const COMMAND_OPTIONS = new Map([
106
112
  ])],
107
113
  ["discover", new Set(["project", "output", "write", "json", "help"])],
108
114
  ["approve", new Set(["project", "proposal", "pending", "ids", "by", "rationale", "write", "json", "full-json", "help"])],
109
- ["context", new Set(["project", "path", "task", "locale", "json", "help"])],
115
+ ["context", new Set(["project", "path", "locate", "task", "locale", "json", "help"])],
110
116
  ["context-query", new Set(["project", "input", "previous", "json", "prompt", "help"])],
111
117
  ["coverage-audit", new Set(["project", "changed-path", "json", "help"])],
112
118
  ["index-context", new Set(["project", "write", "json", "help"])],
@@ -233,6 +239,8 @@ function itemInput(options) {
233
239
  scope: required(options, "scope"),
234
240
  scopePath: options["scope-path"],
235
241
  overrides: options.overrides ?? [],
242
+ requiredSources: options["required-source"] ?? [],
243
+ conditionalSources: options["conditional-source"] ?? [],
236
244
  verification: options.verification,
237
245
  verificationSource: options["verification-source"],
238
246
  verificationExpectedPresent: hasExpected,
@@ -372,6 +380,44 @@ async function runCommand(command, options) {
372
380
  const summary = `Project Context: ${result.status.initialization.state}; health ${result.status.health}; AI Entry ${result.status.entry.state}.\n`;
373
381
  return { exitCode: result.exitCode, stdout: jsonOrText(options, result.status, summary), stderr: "" };
374
382
  }
383
+ if (command === "project-brief") {
384
+ const view = options.view ?? "legacy";
385
+ if (!["legacy", "work"].includes(view)) fail("argument-conflict", "--view must be legacy or work");
386
+ const brief = await buildProjectBrief(root, { view });
387
+ if (view === "work") return { exitCode: brief.delivery.status === "withheld" || brief.connection !== "available" ? 1 : 0, stdout: jsonOrText(options, brief, renderWorkView(brief)), stderr: "" };
388
+ const active = brief.tasks.filter((task) => task.lifecycle === "active");
389
+ const invalid = brief.tasks.filter((task) => task.status === "invalid-record");
390
+ const summary = [
391
+ `已确认:项目 ${brief.project?.id ?? "未初始化"};治理 ${brief.projectHealth};已发现 ${brief.tasks.length} 条任务记录。`,
392
+ `待核实:${brief.findingCodes.join("、") || "无项目治理缺口"}${invalid.length ? `;${invalid.length} 条任务记录无效` : ""}。`,
393
+ `需要你决定:${active.length > 1 ? `从 ${active.length} 条活动任务中选择 taskId` : active.length === 1 ? `确认是否继续 ${active[0].taskId}` : "有新任务时提供目标和依据"}。`,
394
+ `接下来会做什么:${brief.nextAction}。`,
395
+ ].join("\n") + "\n";
396
+ return { exitCode: brief.connection === "available" || brief.connection === "not-connected" ? 0 : 1, stdout: jsonOrText(options, brief, summary), stderr: "" };
397
+ }
398
+ if (command === "task-handoff") {
399
+ const view = options.view ?? "legacy";
400
+ if (!["legacy", "work"].includes(view)) fail("argument-conflict", "--view must be legacy or work");
401
+ const inputPath = oneListValue(options, "input");
402
+ let stdinText;
403
+ if (inputPath === "-") {
404
+ let chunks = "";
405
+ for await (const chunk of process.stdin) {
406
+ chunks += chunk;
407
+ if (Buffer.byteLength(chunks) > 512 * 1024) fail("task-handoff-schema-invalid", "stdin handoff input exceeds 512 KiB");
408
+ }
409
+ stdinText = chunks;
410
+ }
411
+ const bundle = await buildTaskHandoff(root, inputPath, stdinText, { view });
412
+ const summary = [
413
+ `已确认:任务 ${bundle.taskIdentity.taskId};当前意图 ${bundle.intent};状态 ${bundle.taskStatus}。`,
414
+ `待核实:${bundle.gaps.map((gap) => `${gap.code} (${gap.id})`).join("、") || "无当前缺口"}。`,
415
+ `需要你决定:${bundle.questions.filter((question) => question.blockingNow).map((question) => question.question).join("、") || "无当前阻断决定"}。`,
416
+ `接下来会做什么:${bundle.nextAction};${bundle.gate.instruction}`,
417
+ ].join("\n") + "\n";
418
+ const exitCode = bundle.taskStatus === "blocked" ? 2 : ["ready-to-implement", "ready-for-read-only", "inactive"].includes(bundle.taskStatus) ? 0 : 1;
419
+ return { exitCode: view === "work" && bundle.delivery.status === "withheld" ? bundle.taskStatus === "blocked" ? 2 : 1 : exitCode, stdout: jsonOrText(options, bundle, view === "work" ? renderWorkView(bundle) : summary), stderr: "" };
420
+ }
375
421
  if (command === "evidence") {
376
422
  const bundle = await buildEvidenceBundleFile(root, oneListValue(options, "input"));
377
423
  const summary = [
@@ -449,7 +495,7 @@ async function runCommand(command, options) {
449
495
  if (command === "coverage-audit") {
450
496
  const audit = await buildCoverageAuditFiles(root, options["changed-path"] ?? []);
451
497
  const summary = `Registration coverage ${audit.registrationCoverage}: ${audit.categories["review-required"].length} review-required candidate(s).\n`;
452
- return { exitCode: audit.registrationCoverage === "review-required" ? 1 : 0, stdout: jsonOrText(options, audit, summary), stderr: "" };
498
+ return { exitCode: ["review-required", "unresolved"].includes(audit.registrationCoverage) ? 1 : 0, stdout: jsonOrText(options, audit, summary), stderr: "" };
453
499
  }
454
500
  if (command === "index-context") {
455
501
  const index = await indexContextFiles(root, { write: options.write });
@@ -479,6 +525,7 @@ async function runCommand(command, options) {
479
525
  path: optionalOneListValue(options, "path"),
480
526
  pointer: options.pointer,
481
527
  reference: options.reference,
528
+ digestMode: options["digest-mode"],
482
529
  }, { write: options.write });
483
530
  const summary = result.action === "unchanged"
484
531
  ? `Source unchanged: ${result.source.id}.\n`
@@ -637,9 +684,19 @@ async function runCommand(command, options) {
637
684
  if (command === "context") {
638
685
  const findings = blockingContextFindings(await checkProject(root, project));
639
686
  if (findings.length > 0) fail("context-blocked", "context generation is blocked by contract or source findings", { exitCode: 1, details: { findings } });
640
- const paths = required(options, "path");
641
- const content = renderContextBundle(project.contract, paths, options.task, { locale: contextLocale(options) });
642
- return { exitCode: 0, stdout: options.json ? prettyCanonicalJson({ content }) : content, stderr: "" };
687
+ const hasLocate = options.locate === true;
688
+ const hasPaths = options.path !== undefined;
689
+ if (hasLocate === hasPaths) fail("argument-conflict", "context requires exactly one of --locate or --path");
690
+ if (hasLocate && (typeof options.task !== "string" || options.task.trim().length === 0)) {
691
+ fail("argument-missing", "--locate requires a non-empty --task");
692
+ }
693
+ const bundle = await buildContextBundle(root, project, {
694
+ mode: hasLocate ? "locate" : "targeted",
695
+ paths: options.path ?? [],
696
+ task: options.task,
697
+ locale: contextLocale(options),
698
+ });
699
+ return { exitCode: 0, stdout: options.json ? prettyCanonicalJson(bundle) : bundle.content, stderr: "" };
643
700
  }
644
701
  if (command === "publish") {
645
702
  const findings = blockingContextFindings(await checkProject(root, project));