project-tiny-context-harness 0.2.48 → 0.2.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -115,7 +115,7 @@ npm ci
115
115
  npm run smoke:quickstart
116
116
  npm run preview:pack
117
117
  cd /path/to/your/test-repo
118
- npm install -D /path/to/project-tiny-context-harness/tmp/sdlc/source-preview/package/project-tiny-context-harness-0.2.42.tgz
118
+ npm install -D /path/to/project-tiny-context-harness/tmp/sdlc/source-preview/package/project-tiny-context-harness-0.2.50.tgz
119
119
  npx --no-install sdlc-harness init --adopt
120
120
  make validate-context
121
121
  ```
@@ -228,8 +228,9 @@ Use `npx --no-install sdlc-harness ...` only when you explicitly want the alread
228
228
  | Development engineer Skill | `<harnessRoot>/skills/context_development_engineer/SKILL.md` | Handles explicit development-engineering requests and writes durable engineering conclusions to `project_context/**`. |
229
229
  | Full project context export Skill | `<harnessRoot>/skills/context_full_project_export/SKILL.md` | Handles explicit full-project or code-level export requests and uses `export-context --all`, `--full` or `--code` to create temporary artifacts under `tmp/sdlc/context-exports/**`. |
230
230
  | Project-local Skills | `<harnessRoot>/skills/<role>/SKILL.md` | Optional local product/design/development Skills created by the project, such as `product_plan`, `uiux_design` or `development_engineer`. They supersede package-managed default Skills when more specific, are not overwritten by `sync`, and should keep front matter trigger keywords aligned with the project `AGENTS.md` role-trigger rule. |
231
- | Managed file sync | `make sdlc-sync` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness sync` | Refreshes package-managed guidance, default Skills, Makefile include, context templates, tools and workflow YAML. It does not perform semantic Context generation. |
232
- | Upgrade | `make sdlc-upgrade` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade` | Runs safe migrations and `sync`, including Schema v4 Context graph manifest creation when missing. |
231
+ | Managed file sync | `make sdlc-sync` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness sync` | Refreshes package-managed guidance, default Skills, Makefile include, context templates, tools and workflow YAML. It does not run migrations or perform semantic Context generation; when migration work is pending it refuses to write and tells you to run `upgrade`. |
232
+ | Upgrade | `make sdlc-upgrade` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade` | Default command after updating the npm package. Builds an upgrade plan, applies `safe_pending` migrations, runs `sync` and `doctor`, and exits non-zero when manual or blocked follow-up remains. |
233
+ | Upgrade check | `npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade --check [--json]` | Checks the upgrade plan without writing files. Reports `safe_pending`, `manual_required` and `blocked`; exits non-zero when any work remains. |
233
234
  | Combined project export | `npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --all [--check]` | Creates both default temporary exports under `tmp/sdlc/context-exports/**`. |
234
235
  | Project Context export | `npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --full [--output tmp/sdlc/context-exports/name.md] [--check]` | Creates a temporary Context summary artifact. It is not Context and must not be registered in `project_context/context.toml`. |
235
236
  | Code implementation export | `npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --code [--output tmp/sdlc/context-exports/name.md] [--check]` | Creates a temporary single-file code implementation artifact. It is not Context and must not be registered in `project_context/context.toml`. |
@@ -370,11 +371,39 @@ $EDITOR <harnessRoot>/skills/uiux_design/SKILL.md
370
371
 
371
372
  When a project-local Skill and a package-managed default Skill both apply, agents should use the more specific project-local Skill first. The local Skill should keep durable conclusions in `project_context/**` and `DESIGN.md`. Its front matter `description` should stay aligned with the matching default `context_*` Skill and the project `AGENTS.md` role-trigger rule; update both the local Skill and agent guidance when adding or narrowing product/design/development trigger terms. `sync` does not merge Skill overrides and does not overwrite separate project-local Skills. Existing `<harnessRoot>/pjsdlc_managed/override_skills/*.md` files should be migrated into standalone project-local Skills before running `sync`.
372
373
 
373
- ## Sync And Upgrade Boundary
374
-
375
- `sync` is intentionally narrow. It refreshes managed files and never generates project semantics.
376
-
377
- `upgrade` performs safe package migrations and `sync`. The former migration command has been removed because existing users have completed migration.
374
+ ## Sync And Upgrade Boundary
375
+
376
+ `sync` is intentionally narrow. It refreshes managed files and never generates project semantics. `sync` does not run migrations; if it detects `safe_pending`, `manual_required` or `blocked` migration work, it refuses to write and points the user to `upgrade`.
377
+
378
+ After updating the package, run `sdlc-harness upgrade`. Use `sync` only when release notes say the update is `sync-only`.
379
+
380
+ `upgrade` first builds an upgrade plan, applies only `safe_pending` migrations, then runs `sync` and `doctor`. If `manual_required` or `blocked` items remain, the command exits non-zero and prints follow-up. `upgrade --check` performs the same planning step without writing files; `upgrade --check --json` is intended for release checks and CI.
381
+
382
+ Release update modes:
383
+
384
+ | Update mode | What to run | Meaning |
385
+ |---|---|---|
386
+ | `sync-only` | `sdlc-harness sync` | The release changes only package-managed assets. No migrations are required. |
387
+ | `upgrade-required` | `sdlc-harness upgrade` | The release includes safe mechanical migrations and managed asset refresh. |
388
+ | `manual-required` | `sdlc-harness upgrade`, then manual follow-up | The release includes items that cannot be mechanically changed without user intent. |
389
+
390
+ Migration statuses:
391
+
392
+ | Status | Meaning |
393
+ |---|---|
394
+ | `safe_pending` | A known Harness schema, config or path convention can be migrated mechanically. |
395
+ | `manual_required` | The path is in migration scope, but the Harness cannot prove the right semantic role or user intent. |
396
+ | `blocked` | A target conflict or overwrite risk prevents a safe write. |
397
+
398
+ Examples:
399
+
400
+ - `project_context/modules/main.md` -> `project_context/areas/main.md` is safe when the target does not already exist.
401
+ - Missing `project_context/context.toml` can receive a conservative baseline manifest.
402
+ - `project_context/areas/main/verification.md` can be registered as `verification` by path convention.
403
+ - `project_context/areas/payment/api.md` without a manifest role is `manual_required`; the Harness does not guess whether it is an area, contract, foundation or implementation index.
404
+ - If the target already exists, the migration is `blocked` and no file is overwritten.
405
+
406
+ The former migration command has been removed because existing users have completed that migration path.
378
407
 
379
408
  ## Common Commands
380
409
 
@@ -382,13 +411,15 @@ When a project-local Skill and a package-managed default Skill both apply, agent
382
411
  npx --yes --package project-tiny-context-harness@latest sdlc-harness init
383
412
  npx --yes --package project-tiny-context-harness@latest sdlc-harness init --adopt
384
413
  npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --all
385
- npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --full
386
- npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --code
387
- make sdlc-sync
388
- make sdlc-upgrade
389
- npx --yes --package project-tiny-context-harness@latest sdlc-harness validate-context
390
- npx --yes --package project-tiny-context-harness@latest sdlc-harness doctor
391
- make sdlc-doctor
414
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --full
415
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --code
416
+ make sdlc-sync
417
+ make sdlc-upgrade
418
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade --check
419
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade --check --json
420
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness validate-context
421
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness doctor
422
+ make sdlc-doctor
392
423
  make validate-context
393
424
  make validate-harness
394
425
  ```
package/assets/README.md CHANGED
@@ -94,7 +94,7 @@ That smoke packs the local workspace, installs it into a disposable repo, runs `
94
94
  ```sh
95
95
  npm run preview:pack
96
96
  cd /path/to/your/test-repo
97
- npm install -D /path/to/project-tiny-context-harness/tmp/sdlc/source-preview/package/project-tiny-context-harness-0.2.42.tgz
97
+ npm install -D /path/to/project-tiny-context-harness/tmp/sdlc/source-preview/package/project-tiny-context-harness-0.2.50.tgz
98
98
  npx --no-install sdlc-harness init --adopt
99
99
  make validate-context
100
100
  ```
@@ -287,23 +287,60 @@ Use `npx --no-install sdlc-harness ...` only when you explicitly want the alread
287
287
 
288
288
  ## Core Commands
289
289
 
290
- | Command | Purpose |
291
- |---|---|
292
- | `npx --yes --package project-tiny-context-harness@latest sdlc-harness init` | Non-destructively installs Minimal Context Harness into the current project. |
293
- | `make sdlc-sync` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness sync` | Refreshes managed guidance, default Skills, Makefile include, tools and templates. It does not generate project semantics. |
294
- | `make sdlc-upgrade` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade` | Runs safe package migrations and `sync`, including Schema v4 Context graph manifest creation when missing. |
290
+ | Command | Purpose |
291
+ |---|---|
292
+ | `npx --yes --package project-tiny-context-harness@latest sdlc-harness init` | Non-destructively installs Minimal Context Harness into the current project. |
293
+ | `make sdlc-sync` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness sync` | Refreshes managed guidance, default Skills, Makefile include, tools and templates. It does not run migrations or generate project semantics; when migration work is pending it refuses to write and tells you to run `upgrade`. |
294
+ | `make sdlc-upgrade` or `npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade` | Default command after updating the npm package. Builds an upgrade plan, applies `safe_pending` migrations, runs `sync` and `doctor`, and exits non-zero when manual or blocked follow-up remains. |
295
+ | `npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade --check [--json]` | Checks the upgrade plan without writing files. Reports `safe_pending`, `manual_required` and `blocked`; exits non-zero when any work remains. |
295
296
  | `npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --all [--check]` | Creates both default temporary exports under `tmp/sdlc/context-exports/**`. |
296
297
  | `npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --full [--output tmp/sdlc/context-exports/name.md] [--check]` | Creates a temporary project Context summary Markdown artifact. |
297
298
  | `npx --yes --package project-tiny-context-harness@latest sdlc-harness export-context --code [--output tmp/sdlc/context-exports/name.md] [--check]` | Creates a temporary single-file code implementation Markdown artifact. |
298
- | `npx --yes --package project-tiny-context-harness@latest sdlc-harness validate-context` | Checks minimum project recovery fields, Context graph metadata, declared paths/roles and fake test-execution claims. |
299
+ | `npx --yes --package project-tiny-context-harness@latest sdlc-harness validate-context` | Checks minimum project recovery fields, Context graph metadata, declared paths/roles and fake test-execution claims. |
299
300
  | `make validate-context` | Makefile wrapper for `validate-context`. |
300
301
  | `make validate-harness` | Compatibility alias for `validate-context` in vNext projects. |
301
- | `sdlc-harness package sync-source` | Maintainer-only command to sync source workspace assets into `packages/sdlc-harness/assets/**`. |
302
- | `sdlc-harness package check-source` | Maintainer-only drift check for package canonical assets. |
303
-
304
- ## Minimal Context Files
305
-
306
- `project_context/global.md` is the first file a fresh agent should read. It contains:
302
+ | `sdlc-harness package sync-source` | Maintainer-only command to sync source workspace assets into `packages/sdlc-harness/assets/**`. |
303
+ | `sdlc-harness package check-source` | Maintainer-only drift check for package canonical assets. |
304
+
305
+ ## Updating Existing Projects
306
+
307
+ After updating the package, run `sdlc-harness upgrade`. Use `sync` only when release notes say the update is `sync-only`; sync does not run migrations.
308
+
309
+ ```sh
310
+ npm install -D project-tiny-context-harness@latest
311
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade --check
312
+ npx --yes --package project-tiny-context-harness@latest sdlc-harness upgrade
313
+ ```
314
+
315
+ Release notes and release readiness use this update mode vocabulary:
316
+
317
+ | Update mode | What to run | Meaning |
318
+ |---|---|---|
319
+ | `sync-only` | `sdlc-harness sync` | The release changes only package-managed assets. No migrations are required. |
320
+ | `upgrade-required` | `sdlc-harness upgrade` | The release includes safe mechanical migrations and managed asset refresh. |
321
+ | `manual-required` | `sdlc-harness upgrade`, then manual follow-up | The release includes items that cannot be mechanically changed without user intent. |
322
+
323
+ `upgrade --check` prints the plan without writing files. The plan groups work as:
324
+
325
+ | Status | Meaning |
326
+ |---|---|
327
+ | `safe_pending` | The Harness can prove the change is inside a known Harness-owned schema, config or path convention and can apply it mechanically. |
328
+ | `manual_required` | The file is in migration scope, but the Harness cannot prove the right semantic role or user intent. It prints the path and follow-up. |
329
+ | `blocked` | A safe target cannot be written, usually because the destination already exists or another conflict would require overwriting user content. |
330
+
331
+ `upgrade` promises to refresh package-managed assets, apply known safe migrations, avoid overwriting user custom content, expose manual-required migration scope, and run `doctor` / `validate-context` style diagnostics so remaining problems are visible. It does not automatically understand the user's project semantics, decide every Context role, repair project-local Skills, invent business verification paths, update product/deployment facts or turn an old project into the current best-practice shape.
332
+
333
+ Examples:
334
+
335
+ - `project_context/modules/main.md` -> `project_context/areas/main.md` is a safe mechanical migration when the target does not already exist.
336
+ - A missing `project_context/context.toml` can receive a conservative baseline manifest.
337
+ - `project_context/areas/main/verification.md` can be registered as a `verification` role by path convention.
338
+ - `project_context/areas/payment/api.md` without a manifest role is reported as `manual_required`; the Harness does not guess whether it is an area, contract, foundation or implementation index.
339
+ - If `project_context/areas/main.md` already exists while `project_context/modules/main.md` still exists, the migration is `blocked` and no file is overwritten.
340
+
341
+ ## Minimal Context Files
342
+
343
+ `project_context/global.md` is the first file a fresh agent should read. It contains:
307
344
 
308
345
  - project goal
309
346
  - non-goals / boundaries
@@ -6,8 +6,8 @@ SDLC_HARNESS ?= $(if $(wildcard packages/sdlc-harness/dist/cli.js),node packages
6
6
  help:
7
7
  @echo "Minimal Context Harness commands"
8
8
  @echo " make sdlc-doctor Diagnose Harness root, core package and schema version"
9
- @echo " make sdlc-sync Refresh managed guidance, Context templates, default Skills and tools"
10
- @echo " make sdlc-upgrade Run safe upgrade migrations and refresh managed assets"
9
+ @echo " make sdlc-sync Refresh managed assets; refuses when upgrade migrations are pending"
10
+ @echo " make sdlc-upgrade Run safe upgrade migrations, sync managed assets and doctor"
11
11
  @echo " make validate-context Check whether project_context/** supports context recovery"
12
12
  @echo " make validate-harness Compatibility alias for validate-context"
13
13
  @echo " make test-all Run the project regression suite after replacing this placeholder"
@@ -39,15 +39,16 @@ Project-specific engineering rules belong in a separate project-local Skill unde
39
39
  - 对候选点说明当前重复 / 耦合证据、抽象后的边界、收益、风险和是否值得现在做。
40
40
  - 默认只实施高收益、低风险、语义稳定的候选项。
41
41
  - 不为一次性代码、不稳定语义或纯粹好看的架构做抽象。
42
- 13. 需要沉淀长期事实时,只更新 `project_context/**`:
43
- - 全局工程取舍、跨产品域索引或当前状态写入 `global.md`。
44
- - 产品域 API、数据契约、关键约束、入口和风险写入对应 area / subdomain Context。
45
- - 跨域接口语义写入 `context_role: contract` manifest role 为 `contract` 的 Context;关键重复验证路径写入 `verification`;关键部署、运行拓扑或云端初始化路径写入 `deployment`;代码入口索引用 `implementation-index`;底层理论源用 `foundation`;历史归档索引用 `archive`。
46
- - context unit 可新增 `project_context/areas/<unit>.md`,并更新 `global.md#Context Index`;复杂项目同时更新 `project_context/context.toml`。
47
- - 如果 `upgrade` 自动把深层 `.md` 注册成 area,但语义上更像 foundation / contract / archive,后续应显式调整 manifest role;不要依赖自动迁移判断语义。
48
- 14. 实现收尾时做 `Contract Conformance` Context drift check:确认代码没有引入未沉淀的长期事实,且 Context 没有退化成普通实现摘要;交付说明只报告轻量状态:`Context: 已更新 ...` `Context: 本次无长期事实变化`。Conformance 说明本次契约满足情况、未满足或延期项和验证入口;一次性证据、截图结果、测试日志、任务契约和实现摘要不写入 Context。
49
- 15. Context 只能声明验证 / 部署关键路径或验收信号,不能伪造“测试已通过”或“部署已成功”。
50
- 16. Verification / Deployment Role Context 只记录长期可复用的重复执行路径事实:特殊准备、最短命令或路径、预期阶段 / 信号、可接受 warning、已排除的重复探索点。不要记录一次性测试日志、完整输出、临时 JSON、CI artifact、测试报告、release ledger、secret、token、cookie、device id 或 raw payload。
42
+ 13. 当人工流程呈现重复、确定性、容易漏步骤或顺序影响正确性时,主动评估是否应沉淀为 repo-local tool/script。脚本应放在 owning module 的工具目录并配测试;可恢复的执行入口、参数约束和适用边界写入对应 verification / deployment Context。Skill 只记录这类脚本化机会识别原则,不承载具体模块命令、provider id、artifact 路径或一次性运行结果。
43
+ 14. 需要沉淀长期事实时,只更新 `project_context/**`:
44
+ - 全局工程取舍、跨产品域索引或当前状态写入 `global.md`。
45
+ - 产品域 API、数据契约、关键约束、入口和风险写入对应 area / subdomain Context
46
+ - 跨域接口语义写入 `context_role: contract` manifest role 为 `contract` Context;关键重复验证路径写入 `verification`;关键部署、运行拓扑或云端初始化路径写入 `deployment`;代码入口索引用 `implementation-index`;底层理论源用 `foundation`;历史归档索引用 `archive`。
47
+ - context unit 可新增 `project_context/areas/<unit>.md`,并更新 `global.md#Context Index`;复杂项目同时更新 `project_context/context.toml`。
48
+ - 如果 `upgrade` 自动把深层 `.md` 注册成 area,但语义上更像 foundation / contract / archive,后续应显式调整 manifest role;不要依赖自动迁移判断语义。
49
+ 15. 实现收尾时做 `Contract Conformance` 和 Context drift check:确认代码没有引入未沉淀的长期事实,且 Context 没有退化成普通实现摘要;交付说明只报告轻量状态:`Context: 已更新 ...` 或 `Context: 本次无长期事实变化`。Conformance 说明本次契约满足情况、未满足或延期项和验证入口;一次性证据、截图结果、测试日志、任务契约和实现摘要不写入 Context。
50
+ 16. Context 只能声明验证 / 部署关键路径或验收信号,不能伪造“测试已通过”或“部署已成功”。
51
+ 17. Verification / Deployment Role Context 只记录长期可复用的重复执行路径事实:特殊准备、最短命令或路径、预期阶段 / 信号、可接受 warning、已排除的重复探索点。不要记录一次性测试日志、完整输出、临时 JSON、CI artifact、测试报告、release ledger、secret、token、cookie、device id 或 raw payload。
51
52
 
52
53
  ## UI 实现对齐
53
54
 
@@ -21,8 +21,9 @@ export function help() {
21
21
  console.log(`sdlc-harness commands:
22
22
  init [--adopt] [--harness-folder <path>]
23
23
  Initialize/adopt a project; without --harness-folder, choose target agent first
24
- sync Materialize canonical assets into the workspace
25
- upgrade Run migrations and then sync
24
+ sync Refresh managed assets; refuses when upgrade migrations are pending
25
+ upgrade [--check] [--json]
26
+ Run safe migrations, sync managed assets and doctor
26
27
  doctor Diagnose project configuration and drift
27
28
  export-context --full|--code|--all [--output <path>] [--check]
28
29
  Export a temporary Context summary or code implementation Markdown artifact
@@ -1 +1 @@
1
- export declare function upgrade(): Promise<void>;
1
+ export declare function upgrade(args?: string[]): Promise<void>;
@@ -1,7 +1,62 @@
1
+ import { createUpgradePlan, formatUpgradePlan, hasUpgradePlanWork, updateModeForPlan } from "../lib/migrations.js";
1
2
  import { runUpgrade } from "../lib/upgrade.js";
2
- export async function upgrade() {
3
+ export async function upgrade(args = []) {
4
+ const options = parseArgs(args);
5
+ if (options.help) {
6
+ printHelp();
7
+ return;
8
+ }
9
+ if (options.check) {
10
+ const plan = await createUpgradePlan(process.cwd());
11
+ if (options.json) {
12
+ console.log(JSON.stringify({ mode: updateModeForPlan(plan), ...plan }, null, 2));
13
+ }
14
+ else {
15
+ for (const line of formatUpgradePlan(plan)) {
16
+ console.log(line);
17
+ }
18
+ }
19
+ if (hasUpgradePlanWork(plan)) {
20
+ process.exitCode = 1;
21
+ }
22
+ return;
23
+ }
3
24
  const report = await runUpgrade(process.cwd());
25
+ if (options.json) {
26
+ console.log(JSON.stringify({ lines: report }, null, 2));
27
+ return;
28
+ }
4
29
  for (const line of report) {
5
30
  console.log(line);
6
31
  }
7
32
  }
33
+ function parseArgs(args) {
34
+ const options = { check: false, json: false, help: false };
35
+ for (const arg of args) {
36
+ if (arg === "--check") {
37
+ options.check = true;
38
+ }
39
+ else if (arg === "--json") {
40
+ options.json = true;
41
+ }
42
+ else if (arg === "--help" || arg === "-h") {
43
+ options.help = true;
44
+ }
45
+ else {
46
+ throw new Error(`unknown upgrade argument: ${arg}`);
47
+ }
48
+ }
49
+ return options;
50
+ }
51
+ function printHelp() {
52
+ console.log(`sdlc-harness upgrade:
53
+ upgrade Run safe migrations, sync managed assets and doctor
54
+ upgrade --check Print the upgrade plan without writing files
55
+ upgrade --check --json
56
+ Print the upgrade plan as JSON
57
+
58
+ Update modes:
59
+ sync-only No migrations are pending
60
+ upgrade-required Safe migrations are pending
61
+ manual-required Manual review or blockers are present`);
62
+ }
@@ -1,11 +1,39 @@
1
+ export type UpgradePlanItemStatus = "safe_pending" | "manual_required" | "blocked";
2
+ export interface UpgradePlanItem {
3
+ id: string;
4
+ introducedIn: string;
5
+ description: string;
6
+ scope: string;
7
+ status: UpgradePlanItemStatus;
8
+ message: string;
9
+ path?: string;
10
+ }
11
+ export interface UpgradePlan {
12
+ safe_pending: UpgradePlanItem[];
13
+ manual_required: UpgradePlanItem[];
14
+ blocked: UpgradePlanItem[];
15
+ }
16
+ export type ReleaseUpdateMode = "sync-only" | "upgrade-required" | "manual-required";
1
17
  export interface Migration {
2
- from: string;
3
- to: string;
18
+ id: string;
19
+ introducedIn: string;
4
20
  description: string;
21
+ scope: string;
22
+ risk: "safe" | "manual";
23
+ manualMessage: string;
24
+ detect(projectRoot: string, root: string, migrationId: string): Promise<UpgradePlanItem[]>;
25
+ apply?(projectRoot: string, root: string, report: MigrationReport): Promise<void>;
26
+ verify(projectRoot: string, root: string): Promise<void>;
5
27
  }
6
- export declare const migrations: Migration[];
7
28
  export interface MigrationReport {
8
29
  changed: string[];
9
30
  skipped: string[];
31
+ manualRequired: UpgradePlanItem[];
32
+ blocked: UpgradePlanItem[];
10
33
  }
34
+ export declare const migrations: Migration[];
35
+ export declare function createUpgradePlan(projectRoot: string): Promise<UpgradePlan>;
36
+ export declare function hasUpgradePlanWork(plan: UpgradePlan): boolean;
37
+ export declare function updateModeForPlan(plan: UpgradePlan): ReleaseUpdateMode;
38
+ export declare function formatUpgradePlan(plan: UpgradePlan): string[];
11
39
  export declare function runMigrations(projectRoot: string): Promise<MigrationReport>;
@@ -2,21 +2,135 @@ import path from "node:path";
2
2
  import { promises as fs } from "node:fs";
3
3
  import { CONTEXT_MANIFEST_PATH, contextManifestFromExistingAreas } from "./context-manifest.js";
4
4
  import { architectureContextTemplate, areaContextTemplate, globalContextTemplate, verificationContextTemplate } from "./context-templates.js";
5
+ import { CURRENT_SCHEMA_VERSION } from "./constants.js";
5
6
  import { defaultConfig, readConfig } from "./config.js";
6
7
  import { createDesignMdIfMissing, DESIGN_MD_PATH } from "./design-md.js";
7
8
  import { ensureDir, listFiles, pathExists, readText, writeTextIfChanged } from "./fs.js";
8
9
  import { harnessConfigPath, harnessRoot } from "./harness-root.js";
9
10
  import { stringifyYaml } from "./yaml.js";
10
- export const migrations = [];
11
+ async function verifyNoop() {
12
+ return;
13
+ }
14
+ export const migrations = [
15
+ {
16
+ id: "schema-v4-config-refresh",
17
+ introducedIn: "0.2.0",
18
+ description: "Refresh Harness config core metadata and managed-file list for Schema v4.",
19
+ scope: "<harnessRoot>/config.yaml",
20
+ risk: "safe",
21
+ manualMessage: "Review Harness config manually if custom managed-file paths still drift after upgrade.",
22
+ detect: detectConfigRefresh,
23
+ apply: migrateConfig,
24
+ verify: verifyNoop
25
+ },
26
+ {
27
+ id: "legacy-modules-to-areas",
28
+ introducedIn: "0.2.0",
29
+ description: "Move legacy project_context/modules/**/*.md files to project_context/areas/**/*.md.",
30
+ scope: "project_context/modules/**",
31
+ risk: "safe",
32
+ manualMessage: "Resolve target conflicts manually; the Harness will not overwrite existing area Context files.",
33
+ detect: detectLegacyModulesToAreas,
34
+ apply: migrateLegacyModulesToAreas,
35
+ verify: verifyNoop
36
+ },
37
+ {
38
+ id: "context-manifest-baseline",
39
+ introducedIn: "0.2.0",
40
+ description: "Create the Schema v4 project_context/context.toml manifest when missing.",
41
+ scope: "project_context/context.toml and project_context/areas/**",
42
+ risk: "safe",
43
+ manualMessage: "Review deep area files without manifest roles and assign explicit context roles when needed.",
44
+ detect: detectContextManifestBaseline,
45
+ apply: migrateContextManifest,
46
+ verify: verifyNoop
47
+ },
48
+ {
49
+ id: "global-context-v4-sections",
50
+ introducedIn: "0.2.0",
51
+ description: "Add missing Schema v4 global Context sections and rewrite legacy module links.",
52
+ scope: "project_context/global.md",
53
+ risk: "safe",
54
+ manualMessage: "Review global Context manually if project-specific long-term facts need refinement.",
55
+ detect: detectGlobalContextSections,
56
+ apply: migrateGlobalContextSections,
57
+ verify: verifyNoop
58
+ },
59
+ {
60
+ id: "design-md-baseline",
61
+ introducedIn: "0.2.0",
62
+ description: "Create DESIGN.md for existing Harness projects when missing.",
63
+ scope: "DESIGN.md",
64
+ risk: "safe",
65
+ manualMessage: "Review the starter design baseline and replace it with project-specific visual facts when available.",
66
+ detect: detectDesignMdBaseline,
67
+ apply: migrateDesignMd,
68
+ verify: verifyNoop
69
+ },
70
+ {
71
+ id: "deprecated-skill-overrides",
72
+ introducedIn: "0.2.0",
73
+ description: "Report deprecated managed skill overrides that must move to standalone project-local Skills.",
74
+ scope: "<harnessRoot>/pjsdlc_managed/override_skills/**",
75
+ risk: "manual",
76
+ manualMessage: "Move override files into standalone project-local Skills before running sync.",
77
+ detect: detectDeprecatedSkillOverrides,
78
+ verify: verifyNoop
79
+ }
80
+ ];
81
+ export async function createUpgradePlan(projectRoot) {
82
+ const root = await harnessRoot(projectRoot);
83
+ const plan = { safe_pending: [], manual_required: [], blocked: [] };
84
+ for (const migration of migrations) {
85
+ for (const item of await migration.detect(projectRoot, root, migration.id)) {
86
+ plan[item.status].push(item);
87
+ }
88
+ }
89
+ return plan;
90
+ }
91
+ export function hasUpgradePlanWork(plan) {
92
+ return plan.safe_pending.length > 0 || plan.manual_required.length > 0 || plan.blocked.length > 0;
93
+ }
94
+ export function updateModeForPlan(plan) {
95
+ if (plan.manual_required.length > 0 || plan.blocked.length > 0) {
96
+ return "manual-required";
97
+ }
98
+ if (plan.safe_pending.length > 0) {
99
+ return "upgrade-required";
100
+ }
101
+ return "sync-only";
102
+ }
103
+ export function formatUpgradePlan(plan) {
104
+ const lines = [
105
+ `upgrade plan mode=${updateModeForPlan(plan)} safe_pending=${plan.safe_pending.length} manual_required=${plan.manual_required.length} blocked=${plan.blocked.length}`
106
+ ];
107
+ for (const status of ["safe_pending", "manual_required", "blocked"]) {
108
+ for (const item of plan[status]) {
109
+ const location = item.path ? ` ${item.path}` : "";
110
+ lines.push(`${status}: ${item.id}${location} - ${item.message}`);
111
+ }
112
+ }
113
+ return lines;
114
+ }
11
115
  export async function runMigrations(projectRoot) {
12
- const report = { changed: [], skipped: [] };
116
+ const report = { changed: [], skipped: [], manualRequired: [], blocked: [] };
13
117
  const root = await harnessRoot(projectRoot);
14
- await migrateConfig(projectRoot, root, report);
15
- await migrateLegacyModulesToAreas(projectRoot, report);
118
+ const plan = await createUpgradePlan(projectRoot);
119
+ report.manualRequired.push(...plan.manual_required);
120
+ report.blocked.push(...plan.blocked);
121
+ for (const migration of migrations) {
122
+ if (!migration.apply) {
123
+ continue;
124
+ }
125
+ if (!plan.safe_pending.some((item) => item.id === migration.id)) {
126
+ report.skipped.push(migration.id);
127
+ continue;
128
+ }
129
+ await migration.apply(projectRoot, root, report);
130
+ await migration.verify(projectRoot, root);
131
+ }
16
132
  await migrateBaseProjectContext(projectRoot, report);
17
- await migrateContextManifest(projectRoot, report);
18
133
  await migrateManifestModulePaths(projectRoot, report);
19
- await migrateDesignMd(projectRoot, report);
20
134
  return report;
21
135
  }
22
136
  async function migrateBaseProjectContext(projectRoot, report) {
@@ -40,12 +154,30 @@ async function migrateBaseProjectContext(projectRoot, report) {
40
154
  report.skipped.push(relative);
41
155
  }
42
156
  }
43
- await migrateGlobalContextSections(projectRoot, report);
44
157
  }
45
- async function migrateGlobalContextSections(projectRoot, report) {
158
+ async function detectGlobalContextSections(projectRoot, _root, migration) {
46
159
  const relative = "project_context/global.md";
47
160
  const target = path.join(projectRoot, ...relative.split("/"));
48
161
  if (!(await pathExists(target))) {
162
+ return [];
163
+ }
164
+ const original = await readText(target);
165
+ const rewritten = rewriteLegacyModuleReferences(original);
166
+ const needsSections = !hasHeading(rewritten, "Architecture Context") ||
167
+ !hasHeading(rewritten, "Context Graph") ||
168
+ !hasHeading(rewritten, "Context Index");
169
+ if (!needsSections && rewritten === original) {
170
+ return [];
171
+ }
172
+ return [
173
+ item(migration, "safe_pending", relative, "Global Context needs Schema v4 sections or legacy module path rewrites.")
174
+ ];
175
+ }
176
+ async function migrateGlobalContextSections(projectRoot, _root, report) {
177
+ const relative = "project_context/global.md";
178
+ const target = path.join(projectRoot, ...relative.split("/"));
179
+ if (!(await pathExists(target))) {
180
+ report.skipped.push(relative);
49
181
  return;
50
182
  }
51
183
  const original = await readText(target);
@@ -61,14 +193,31 @@ async function migrateGlobalContextSections(projectRoot, report) {
61
193
  additions.push("## Context Index", "", "- See `project_context/context.toml` for the current area and context node list.", "");
62
194
  }
63
195
  if (additions.length === 0 && rewritten === original) {
196
+ report.skipped.push(`${relative}#schema-v4-sections`);
64
197
  return;
65
198
  }
66
199
  const next = additions.length === 0 ? rewritten : `${rewritten.replace(/\s*$/, "\n\n")}${additions.join("\n")}`;
67
200
  if (await writeTextIfChanged(target, next)) {
68
201
  report.changed.push(`${relative}#schema-v4-sections`);
69
202
  }
203
+ else {
204
+ report.skipped.push(`${relative}#schema-v4-sections`);
205
+ }
70
206
  }
71
- async function migrateContextManifest(projectRoot, report) {
207
+ async function detectContextManifestBaseline(projectRoot, _root, migration) {
208
+ const manifestPath = path.join(projectRoot, CONTEXT_MANIFEST_PATH);
209
+ if (await pathExists(manifestPath)) {
210
+ return [];
211
+ }
212
+ const items = [
213
+ item(migration, "safe_pending", CONTEXT_MANIFEST_PATH, "Context graph manifest is missing and can be created from existing area files.")
214
+ ];
215
+ for (const ambiguous of await ambiguousAreaContextFiles(projectRoot)) {
216
+ items.push(item(migration, "manual_required", ambiguous, "Deep area Context cannot be safely role-classified by path alone; review context.toml after upgrade."));
217
+ }
218
+ return items;
219
+ }
220
+ async function migrateContextManifest(projectRoot, _root, report) {
72
221
  const manifestPath = path.join(projectRoot, CONTEXT_MANIFEST_PATH);
73
222
  if (await pathExists(manifestPath)) {
74
223
  report.skipped.push(CONTEXT_MANIFEST_PATH);
@@ -81,7 +230,26 @@ async function migrateContextManifest(projectRoot, report) {
81
230
  report.skipped.push(CONTEXT_MANIFEST_PATH);
82
231
  }
83
232
  }
84
- async function migrateLegacyModulesToAreas(projectRoot, report) {
233
+ async function detectLegacyModulesToAreas(projectRoot, _root, migration) {
234
+ const modulesRoot = path.join(projectRoot, "project_context", "modules");
235
+ const areasRoot = path.join(projectRoot, "project_context", "areas");
236
+ const moduleFiles = (await listFiles(modulesRoot)).filter((file) => file.endsWith(".md")).sort();
237
+ const items = [];
238
+ for (const source of moduleFiles) {
239
+ const relativeToModules = path.relative(modulesRoot, source);
240
+ const sourceRelative = `project_context/modules/${relativeToModules.split(path.sep).join("/")}`;
241
+ const targetRelative = `project_context/areas/${relativeToModules.split(path.sep).join("/")}`;
242
+ const target = path.join(areasRoot, relativeToModules);
243
+ if (await pathExists(target)) {
244
+ items.push(item(migration, "blocked", sourceRelative, `Cannot move to ${targetRelative} because the target already exists.`));
245
+ }
246
+ else {
247
+ items.push(item(migration, "safe_pending", sourceRelative, `Move to ${targetRelative}.`));
248
+ }
249
+ }
250
+ return items;
251
+ }
252
+ async function migrateLegacyModulesToAreas(projectRoot, _root, report) {
85
253
  const modulesRoot = path.join(projectRoot, "project_context", "modules");
86
254
  const areasRoot = path.join(projectRoot, "project_context", "areas");
87
255
  const moduleFiles = (await listFiles(modulesRoot)).filter((file) => file.endsWith(".md")).sort();
@@ -120,7 +288,12 @@ async function migrateManifestModulePaths(projectRoot, report) {
120
288
  report.changed.push(`${CONTEXT_MANIFEST_PATH}#areas-paths`);
121
289
  }
122
290
  }
123
- async function migrateDesignMd(projectRoot, report) {
291
+ async function detectDesignMdBaseline(projectRoot, _root, migration) {
292
+ return (await pathExists(path.join(projectRoot, DESIGN_MD_PATH)))
293
+ ? []
294
+ : [item(migration, "safe_pending", DESIGN_MD_PATH, "DESIGN.md is missing and can be created with the package baseline.")];
295
+ }
296
+ async function migrateDesignMd(projectRoot, _root, report) {
124
297
  if (await createDesignMdIfMissing(projectRoot)) {
125
298
  report.changed.push(DESIGN_MD_PATH);
126
299
  }
@@ -128,6 +301,26 @@ async function migrateDesignMd(projectRoot, report) {
128
301
  report.skipped.push(DESIGN_MD_PATH);
129
302
  }
130
303
  }
304
+ async function detectConfigRefresh(projectRoot, root, migration) {
305
+ const relativeConfigPath = await harnessConfigPath(projectRoot);
306
+ const configPath = path.join(projectRoot, relativeConfigPath);
307
+ if (!(await pathExists(configPath))) {
308
+ return [];
309
+ }
310
+ const config = await readConfig(projectRoot);
311
+ const current = defaultConfig(root);
312
+ const managedMatches = JSON.stringify(config.managed_files) === JSON.stringify(current.managed_files);
313
+ const neverOverwriteHasDefaults = current.never_overwrite.every((entry) => config.never_overwrite.includes(entry));
314
+ if (config.core.package === current.core.package &&
315
+ config.core.schema_version === CURRENT_SCHEMA_VERSION &&
316
+ managedMatches &&
317
+ neverOverwriteHasDefaults) {
318
+ return [];
319
+ }
320
+ return [
321
+ item(migration, "safe_pending", relativeConfigPath, "Harness config needs current package metadata, schema version or managed-file defaults.")
322
+ ];
323
+ }
131
324
  async function migrateConfig(projectRoot, root, report) {
132
325
  const relativeConfigPath = await harnessConfigPath(projectRoot);
133
326
  const configPath = path.join(projectRoot, relativeConfigPath);
@@ -147,6 +340,52 @@ async function migrateConfig(projectRoot, root, report) {
147
340
  report.skipped.push(relativeConfigPath);
148
341
  }
149
342
  }
343
+ async function detectDeprecatedSkillOverrides(projectRoot, root, migration) {
344
+ const overrideRoot = path.join(projectRoot, root, "pjsdlc_managed", "override_skills");
345
+ if (!(await pathExists(overrideRoot))) {
346
+ return [];
347
+ }
348
+ const deprecatedFiles = (await listFiles(overrideRoot))
349
+ .filter((file) => path.basename(file) !== ".gitkeep")
350
+ .map((file) => path.relative(projectRoot, file).split(path.sep).join("/"))
351
+ .sort();
352
+ return deprecatedFiles.map((file) => item(migration, "manual_required", file, "Skill overrides are no longer supported; move rules into a standalone project-local Skill before relying on sync."));
353
+ }
354
+ async function ambiguousAreaContextFiles(projectRoot) {
355
+ const areasRoot = path.join(projectRoot, "project_context", "areas");
356
+ const areaFiles = (await listFiles(areasRoot)).filter((file) => file.endsWith(".md")).sort();
357
+ const ambiguous = [];
358
+ for (const file of areaFiles) {
359
+ const relativeToAreas = path.relative(areasRoot, file).split(path.sep).join("/");
360
+ if (!relativeToAreas.includes("/")) {
361
+ continue;
362
+ }
363
+ if (inferredRoleContext(relativeToAreas)) {
364
+ continue;
365
+ }
366
+ const base = path.basename(relativeToAreas, ".md").toLowerCase();
367
+ if (base === "readme" || base === "index") {
368
+ continue;
369
+ }
370
+ ambiguous.push(`project_context/areas/${relativeToAreas}`);
371
+ }
372
+ return ambiguous;
373
+ }
374
+ function item(migrationId, status, pathLabel, message) {
375
+ const migration = migrations.find((entry) => entry.id === migrationId);
376
+ if (!migration) {
377
+ throw new Error(`Unknown migration id: ${migrationId}`);
378
+ }
379
+ return {
380
+ id: migration.id,
381
+ introducedIn: migration.introducedIn,
382
+ description: migration.description,
383
+ scope: migration.scope,
384
+ status,
385
+ path: pathLabel,
386
+ message
387
+ };
388
+ }
150
389
  function hasHeading(content, heading) {
151
390
  const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
152
391
  return new RegExp(`^##\\s+${escaped}\\s*$`, "im").test(content);
@@ -156,6 +395,16 @@ function rewriteLegacyModuleReferences(content) {
156
395
  .replace(/project_context\/modules\//g, "project_context/areas/")
157
396
  .replace(/\(modules\//g, "(areas/");
158
397
  }
398
+ function inferredRoleContext(relativeToAreas) {
399
+ const normalized = relativeToAreas.toLowerCase();
400
+ if (normalized.endsWith("/verification.md") || normalized === "verification.md") {
401
+ return "verification";
402
+ }
403
+ if (normalized.endsWith("/deployment.md") || normalized === "deployment.md") {
404
+ return "deployment";
405
+ }
406
+ return undefined;
407
+ }
159
408
  function ensureManifestDefaultArea(content) {
160
409
  if (/^\s*default\s*=\s*true\s*$/im.test(content)) {
161
410
  return content;
@@ -3,5 +3,8 @@ export interface SyncReport {
3
3
  skipped: string[];
4
4
  blocked: string[];
5
5
  }
6
+ export interface SyncOptions {
7
+ allowPendingMigrations?: boolean;
8
+ }
6
9
  export declare function emptySyncReport(): SyncReport;
7
- export declare function runSync(projectRoot: string): Promise<SyncReport>;
10
+ export declare function runSync(projectRoot: string, options?: SyncOptions): Promise<SyncReport>;
@@ -6,6 +6,7 @@ import { copyTree, listFiles, pathExists, readText, writeTextIfChanged } from ".
6
6
  import { AGENTS_BLOCK_MARKERS, GITHUB_WORKFLOW_BLOCK_END, GITHUB_WORKFLOW_BLOCK_START, MAKEFILE_BLOCK_END, MAKEFILE_BLOCK_MARKERS, MAKEFILE_BLOCK_START, MANAGED_BLOCK_END, MANAGED_BLOCK_START } from "./managed-file.js";
7
7
  import { packageAssetPath } from "./paths.js";
8
8
  import { assertSupportedSchema } from "./schema-guard.js";
9
+ import { createUpgradePlan, formatUpgradePlan, hasUpgradePlanWork } from "./migrations.js";
9
10
  export function emptySyncReport() {
10
11
  return {
11
12
  changed: [],
@@ -13,11 +14,18 @@ export function emptySyncReport() {
13
14
  blocked: []
14
15
  };
15
16
  }
16
- export async function runSync(projectRoot) {
17
+ export async function runSync(projectRoot, options = {}) {
17
18
  await assertSupportedSchema(projectRoot, "sync");
18
19
  const root = await harnessRoot(projectRoot);
19
20
  const config = await readConfig(projectRoot);
20
21
  const report = emptySyncReport();
22
+ if (!options.allowPendingMigrations) {
23
+ const upgradePlan = await createUpgradePlan(projectRoot);
24
+ if (hasUpgradePlanWork(upgradePlan)) {
25
+ report.blocked.push(`upgrade required before sync: ${formatUpgradePlan(upgradePlan).join(" | ")}`);
26
+ return report;
27
+ }
28
+ }
21
29
  await blockDeprecatedSkillOverrides(projectRoot, root, report);
22
30
  if (report.blocked.length > 0) {
23
31
  return report;
@@ -1,20 +1,30 @@
1
1
  import { runDoctor } from "./doctor.js";
2
- import { runMigrations } from "./migrations.js";
2
+ import { formatUpgradePlan, runMigrations } from "./migrations.js";
3
3
  import { assertSupportedSchema } from "./schema-guard.js";
4
4
  import { runSync } from "./sync-engine.js";
5
5
  export async function runUpgrade(projectRoot) {
6
6
  const lines = [];
7
7
  await assertSupportedSchema(projectRoot, "upgrade");
8
8
  const migrationReport = await runMigrations(projectRoot);
9
- lines.push(`migrations changed=${migrationReport.changed.length} skipped=${migrationReport.skipped.length}`);
10
- const syncReport = await runSync(projectRoot);
9
+ lines.push(`migrations changed=${migrationReport.changed.length} skipped=${migrationReport.skipped.length} manual_required=${migrationReport.manualRequired.length} blocked=${migrationReport.blocked.length}`);
10
+ if (migrationReport.manualRequired.length > 0 || migrationReport.blocked.length > 0) {
11
+ lines.push(...formatUpgradePlan({
12
+ safe_pending: [],
13
+ manual_required: migrationReport.manualRequired,
14
+ blocked: migrationReport.blocked
15
+ }).slice(1));
16
+ }
17
+ const syncReport = await runSync(projectRoot, { allowPendingMigrations: true });
11
18
  lines.push(`sync changed=${syncReport.changed.length} skipped=${syncReport.skipped.length} blocked=${syncReport.blocked.length}`);
12
19
  for (const skipped of syncReport.skipped.filter((line) => line.includes("customized"))) {
13
20
  lines.push(`sync skipped: ${skipped}`);
14
21
  }
15
22
  const doctor = await runDoctor(projectRoot);
16
23
  lines.push(`doctor warnings=${doctor.warnings.length} errors=${doctor.errors.length}`);
17
- if (syncReport.blocked.length > 0 || doctor.errors.length > 0) {
24
+ if (migrationReport.manualRequired.length > 0 ||
25
+ migrationReport.blocked.length > 0 ||
26
+ syncReport.blocked.length > 0 ||
27
+ doctor.errors.length > 0) {
18
28
  throw new Error("upgrade completed with blockers");
19
29
  }
20
30
  return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-tiny-context-harness",
3
- "version": "0.2.48",
3
+ "version": "0.2.50",
4
4
  "description": "Minimal project memory and validation harness for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "Seven128",