project-tiny-context-harness 0.2.49 → 0.2.51

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.
@@ -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;
@@ -1,3 +1,3 @@
1
- # Migrations
2
-
3
- Schema migrations for `.harness/config.yaml` and managed file layout belong here.
1
+ # Migrations
2
+
3
+ Schema migrations for `.harness/config.yaml` and managed file layout belong here.
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "project-tiny-context-harness",
3
- "version": "0.2.49",
4
- "description": "Minimal project memory and validation harness for AI coding agents.",
5
- "license": "MIT",
6
- "author": "Seven128",
7
- "homepage": "https://github.com/Seven128/project-tiny-context-harness#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/Seven128/project-tiny-context-harness.git",
11
- "directory": "packages/sdlc-harness"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/Seven128/project-tiny-context-harness/issues"
15
- },
16
- "keywords": [
17
- "ai-agents",
18
- "coding-agent",
19
- "codex",
20
- "claude-code",
21
- "cursor",
22
- "gemini-cli",
23
- "opencode",
24
- "agent-context",
25
- "context-engineering",
26
- "context-management",
27
- "agents-md",
28
- "project-memory",
29
- "agent-memory",
30
- "ai-coding",
31
- "multi-agent",
32
- "llm",
33
- "developer-tools",
34
- "developer-productivity",
35
- "cli",
36
- "sdlc",
37
- "workflow"
38
- ],
39
- "type": "module",
40
- "bin": {
41
- "sdlc-harness": "dist/cli.js"
42
- },
43
- "files": [
44
- "README.md",
45
- "dist",
46
- "assets",
47
- "migrations",
48
- "source-mappings.yaml"
49
- ],
50
- "scripts": {
51
- "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
52
- "typecheck": "tsc -p tsconfig.json --noEmit",
53
- "test": "npm run build && node --test ../../tests/sdlc-harness/*.test.mjs",
54
- "prepack": "npm run build"
55
- },
56
- "engines": {
57
- "node": ">=20"
58
- },
59
- "dependencies": {
60
- "@google/design.md": "^0.2.0",
61
- "impeccable": "^2.3.2",
62
- "yaml": "^2.9.0"
63
- },
64
- "devDependencies": {
65
- "@types/node": "^24.0.0",
66
- "typescript": "^5.5.0"
67
- }
68
- }
1
+ {
2
+ "name": "project-tiny-context-harness",
3
+ "version": "0.2.51",
4
+ "description": "Minimal project memory and validation harness for AI coding agents.",
5
+ "license": "MIT",
6
+ "author": "Seven128",
7
+ "homepage": "https://github.com/Seven128/project-tiny-context-harness#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Seven128/project-tiny-context-harness.git",
11
+ "directory": "packages/sdlc-harness"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Seven128/project-tiny-context-harness/issues"
15
+ },
16
+ "keywords": [
17
+ "ai-agents",
18
+ "coding-agent",
19
+ "codex",
20
+ "claude-code",
21
+ "cursor",
22
+ "gemini-cli",
23
+ "opencode",
24
+ "agent-context",
25
+ "context-engineering",
26
+ "context-management",
27
+ "agents-md",
28
+ "project-memory",
29
+ "agent-memory",
30
+ "ai-coding",
31
+ "multi-agent",
32
+ "llm",
33
+ "developer-tools",
34
+ "developer-productivity",
35
+ "cli",
36
+ "sdlc",
37
+ "workflow"
38
+ ],
39
+ "type": "module",
40
+ "bin": {
41
+ "sdlc-harness": "dist/cli.js"
42
+ },
43
+ "files": [
44
+ "README.md",
45
+ "dist",
46
+ "assets",
47
+ "migrations",
48
+ "source-mappings.yaml"
49
+ ],
50
+ "scripts": {
51
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "test": "npm run build && node --test ../../tests/sdlc-harness/*.test.mjs",
54
+ "prepack": "npm run build"
55
+ },
56
+ "engines": {
57
+ "node": ">=20"
58
+ },
59
+ "dependencies": {
60
+ "@google/design.md": "^0.2.0",
61
+ "impeccable": "^2.3.2",
62
+ "yaml": "^2.9.0"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^24.0.0",
66
+ "typescript": "^5.5.0"
67
+ }
68
+ }