create-sdd-project 0.18.0 → 0.18.2
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/lib/adapt-agents.js
CHANGED
|
@@ -91,6 +91,13 @@ const WORKFLOW_CORE_PROJECT_TYPE_RULES = {
|
|
|
91
91
|
[/### UI Changes \(if applicable\)\n\n\[Components to add\/modify\. Reference `docs\/specs\/ui-components\.md`\.\]\n\n/, ''],
|
|
92
92
|
[' / `ui-components.md`', ''],
|
|
93
93
|
],
|
|
94
|
+
// v0.18.2 (Codex R1 finding C4): pr-template.md is project-type adapted —
|
|
95
|
+
// moved from inline replaceInFileFn to this table so smart-diff fallback
|
|
96
|
+
// compare via adaptWorkflowCoreContentForProjectType produces a matching
|
|
97
|
+
// pristine target on backend/frontend installs (no false-preserve).
|
|
98
|
+
'skills/development-workflow/references/pr-template.md': [
|
|
99
|
+
[' / ui-components.md', ''],
|
|
100
|
+
],
|
|
94
101
|
},
|
|
95
102
|
frontend: {
|
|
96
103
|
'skills/development-workflow/SKILL.md': [
|
|
@@ -101,6 +108,9 @@ const WORKFLOW_CORE_PROJECT_TYPE_RULES = {
|
|
|
101
108
|
[/### API Changes \(if applicable\)\n\n\[Endpoints to add\/modify\. Reference[^\]]*\]\n\n/, ''],
|
|
102
109
|
['`api-spec.yaml` / ', ''],
|
|
103
110
|
],
|
|
111
|
+
'skills/development-workflow/references/pr-template.md': [
|
|
112
|
+
['api-spec.yaml / ', ''],
|
|
113
|
+
],
|
|
104
114
|
},
|
|
105
115
|
};
|
|
106
116
|
|
|
@@ -243,25 +253,16 @@ function adaptAgentContentForProjectType(dest, config, replaceInFileFn) {
|
|
|
243
253
|
replaceInFileFn(path.join(dest, 'AGENTS.md'), [
|
|
244
254
|
[', `ui-ux-designer`', ''],
|
|
245
255
|
]);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'pr-template.md'), [
|
|
250
|
-
[' / ui-components.md', ''],
|
|
251
|
-
]);
|
|
252
|
-
}
|
|
256
|
+
// v0.18.2 (Codex R1 C4): pr-template.md rules moved to WORKFLOW_CORE_PROJECT_TYPE_RULES
|
|
257
|
+
// (handled by the wfRules loop above) so smart-diff fallback compare can
|
|
258
|
+
// produce a pristine adapted target via adaptWorkflowCoreContentForProjectType.
|
|
253
259
|
// base-standards.mdc: remove ui-ux-designer table row (shared table above)
|
|
254
260
|
replaceInFileFn(
|
|
255
261
|
path.join(dest, 'ai-specs', 'specs', 'base-standards.mdc'),
|
|
256
262
|
BASE_STANDARDS_PROJECT_TYPE_RULES.backend
|
|
257
263
|
);
|
|
258
|
-
} else if (config.projectType === 'frontend') {
|
|
259
|
-
for (const dir of toolDirs) {
|
|
260
|
-
replaceInFileFn(path.join(dest, dir, 'skills', 'development-workflow', 'references', 'pr-template.md'), [
|
|
261
|
-
['api-spec.yaml / ', ''],
|
|
262
|
-
]);
|
|
263
|
-
}
|
|
264
264
|
}
|
|
265
|
+
// v0.18.2: frontend pr-template rule also moved to WORKFLOW_CORE_PROJECT_TYPE_RULES.
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
module.exports = {
|
package/lib/meta.js
CHANGED
|
@@ -273,14 +273,79 @@ function expectedSmartDiffTrackedPaths(aiTools, projectType) {
|
|
|
273
273
|
paths.add('ai-specs/specs/documentation-standards.mdc');
|
|
274
274
|
|
|
275
275
|
// v0.17.1: development-workflow skill core files — filtered by aiTools.
|
|
276
|
-
// bug-workflow, health-check, pm-orchestrator, project-memory are OUT OF
|
|
277
|
-
// SCOPE for v0.17.1 (deferred — see dev/ROADMAP.md "Known follow-ups" item 2).
|
|
278
276
|
for (const dir of toolDirs) {
|
|
279
277
|
paths.add(`${dir}/skills/development-workflow/SKILL.md`);
|
|
280
278
|
paths.add(`${dir}/skills/development-workflow/references/ticket-template.md`);
|
|
281
279
|
paths.add(`${dir}/skills/development-workflow/references/merge-checklist.md`);
|
|
282
280
|
}
|
|
283
281
|
|
|
282
|
+
// v0.18.2: extend smart-diff coverage to remaining SKILL.md entry points +
|
|
283
|
+
// 11 reference files (closes ROADMAP "Known follow-ups" item 1 — long-deferred
|
|
284
|
+
// since v0.17.1). Universal per aiTools (no project-type filtering — skills
|
|
285
|
+
// and reference templates are stack-agnostic except pr-template.md, which
|
|
286
|
+
// has project-type rules in WORKFLOW_CORE_PROJECT_TYPE_RULES so the
|
|
287
|
+
// fallback compare in upgrade-generator.js produces a pristine adapted
|
|
288
|
+
// target).
|
|
289
|
+
for (const dir of toolDirs) {
|
|
290
|
+
// 4 SKILL.md entry points
|
|
291
|
+
paths.add(`${dir}/skills/bug-workflow/SKILL.md`);
|
|
292
|
+
paths.add(`${dir}/skills/health-check/SKILL.md`);
|
|
293
|
+
paths.add(`${dir}/skills/pm-orchestrator/SKILL.md`);
|
|
294
|
+
paths.add(`${dir}/skills/project-memory/SKILL.md`);
|
|
295
|
+
// 7 development-workflow references
|
|
296
|
+
paths.add(`${dir}/skills/development-workflow/references/pr-template.md`);
|
|
297
|
+
paths.add(`${dir}/skills/development-workflow/references/branching-strategy.md`);
|
|
298
|
+
paths.add(`${dir}/skills/development-workflow/references/failure-handling.md`);
|
|
299
|
+
paths.add(`${dir}/skills/development-workflow/references/workflow-example.md`);
|
|
300
|
+
paths.add(`${dir}/skills/development-workflow/references/complexity-guide.md`);
|
|
301
|
+
paths.add(`${dir}/skills/development-workflow/references/add-feature-template.md`);
|
|
302
|
+
paths.add(`${dir}/skills/development-workflow/references/cross-model-review.md`);
|
|
303
|
+
// 1 pm-orchestrator reference
|
|
304
|
+
paths.add(`${dir}/skills/pm-orchestrator/references/pm-session-template.md`);
|
|
305
|
+
// 3 project-memory references
|
|
306
|
+
paths.add(`${dir}/skills/project-memory/references/bugs_template.md`);
|
|
307
|
+
paths.add(`${dir}/skills/project-memory/references/decisions_template.md`);
|
|
308
|
+
paths.add(`${dir}/skills/project-memory/references/key_facts_template.md`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// v0.18.1: shipped slash-commands — preserve user customizations across
|
|
312
|
+
// upgrades. Closes v0.18.0 known limitation where audit-merge.md was
|
|
313
|
+
// wholesale-overwritten (notable since teams may tune drift recipes for
|
|
314
|
+
// their PR/ticket conventions). Same hash decision tree as agents +
|
|
315
|
+
// standards: missing/force → write, hash match → replace, hash mismatch
|
|
316
|
+
// → preserve + .new backup, no hash → fallback content compare.
|
|
317
|
+
const COMMAND_FILES_CLAUDE = [
|
|
318
|
+
'audit-merge.md',
|
|
319
|
+
'context-prompt.md',
|
|
320
|
+
'review-plan.md',
|
|
321
|
+
'review-project.md',
|
|
322
|
+
'review-spec.md',
|
|
323
|
+
];
|
|
324
|
+
const COMMAND_FILES_GEMINI = [
|
|
325
|
+
// Each Claude command has a Gemini twin: a thin TOML wrapper + a body
|
|
326
|
+
// -instructions.md. Both must be tracked since users may edit either.
|
|
327
|
+
'audit-merge.toml',
|
|
328
|
+
'audit-merge-instructions.md',
|
|
329
|
+
'context-prompt.toml',
|
|
330
|
+
'context-prompt-instructions.md',
|
|
331
|
+
'review-plan.toml',
|
|
332
|
+
'review-plan-instructions.md',
|
|
333
|
+
'review-project.toml',
|
|
334
|
+
'review-project-instructions.md',
|
|
335
|
+
'review-spec.toml',
|
|
336
|
+
'review-spec-instructions.md',
|
|
337
|
+
];
|
|
338
|
+
if (aiTools !== 'gemini') {
|
|
339
|
+
for (const cmd of COMMAND_FILES_CLAUDE) {
|
|
340
|
+
paths.add(`.claude/commands/${cmd}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (aiTools !== 'claude') {
|
|
344
|
+
for (const cmd of COMMAND_FILES_GEMINI) {
|
|
345
|
+
paths.add(`.gemini/commands/${cmd}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
284
349
|
return paths;
|
|
285
350
|
}
|
|
286
351
|
|
package/lib/upgrade-generator.js
CHANGED
|
@@ -305,21 +305,45 @@ function generateUpgrade(config) {
|
|
|
305
305
|
const filesToAdapt = new Set();
|
|
306
306
|
|
|
307
307
|
// v0.17.1: before the skills/ wholesale delete-and-copy, save the content
|
|
308
|
-
// of the
|
|
308
|
+
// of the workflow-core files so we can restore them if their hash tells
|
|
309
309
|
// us they were customized. Map keyed by absolute path, value = string or
|
|
310
310
|
// null (null = file didn't exist before upgrade).
|
|
311
|
+
// v0.18.2: extended from 6 paths to 21 paths × tool dirs (max 42 for
|
|
312
|
+
// fullstack-both). Adds 4 SKILL.md entry points + 11 reference files,
|
|
313
|
+
// closing ROADMAP "Known follow-ups" item 1.
|
|
311
314
|
const workflowCoreBackup = new Map();
|
|
312
315
|
const workflowCorePosixPaths = [];
|
|
313
316
|
{
|
|
314
317
|
const dirsForBackup = [];
|
|
315
318
|
if (aiTools !== 'gemini') dirsForBackup.push('.claude');
|
|
316
319
|
if (aiTools !== 'claude') dirsForBackup.push('.gemini');
|
|
320
|
+
const WORKFLOW_CORE_RELATIVE_PATHS = [
|
|
321
|
+
// v0.17.1 (3 files)
|
|
322
|
+
'skills/development-workflow/SKILL.md',
|
|
323
|
+
'skills/development-workflow/references/ticket-template.md',
|
|
324
|
+
'skills/development-workflow/references/merge-checklist.md',
|
|
325
|
+
// v0.18.2: 4 SKILL.md entry points
|
|
326
|
+
'skills/bug-workflow/SKILL.md',
|
|
327
|
+
'skills/health-check/SKILL.md',
|
|
328
|
+
'skills/pm-orchestrator/SKILL.md',
|
|
329
|
+
'skills/project-memory/SKILL.md',
|
|
330
|
+
// v0.18.2: 7 development-workflow references
|
|
331
|
+
'skills/development-workflow/references/pr-template.md',
|
|
332
|
+
'skills/development-workflow/references/branching-strategy.md',
|
|
333
|
+
'skills/development-workflow/references/failure-handling.md',
|
|
334
|
+
'skills/development-workflow/references/workflow-example.md',
|
|
335
|
+
'skills/development-workflow/references/complexity-guide.md',
|
|
336
|
+
'skills/development-workflow/references/add-feature-template.md',
|
|
337
|
+
'skills/development-workflow/references/cross-model-review.md',
|
|
338
|
+
// v0.18.2: 1 pm-orchestrator reference
|
|
339
|
+
'skills/pm-orchestrator/references/pm-session-template.md',
|
|
340
|
+
// v0.18.2: 3 project-memory references
|
|
341
|
+
'skills/project-memory/references/bugs_template.md',
|
|
342
|
+
'skills/project-memory/references/decisions_template.md',
|
|
343
|
+
'skills/project-memory/references/key_facts_template.md',
|
|
344
|
+
];
|
|
317
345
|
for (const dir of dirsForBackup) {
|
|
318
|
-
for (const relSub of
|
|
319
|
-
'skills/development-workflow/SKILL.md',
|
|
320
|
-
'skills/development-workflow/references/ticket-template.md',
|
|
321
|
-
'skills/development-workflow/references/merge-checklist.md',
|
|
322
|
-
]) {
|
|
346
|
+
for (const relSub of WORKFLOW_CORE_RELATIVE_PATHS) {
|
|
323
347
|
const posix = `${dir}/${relSub}`;
|
|
324
348
|
const abs = path.join(dest, ...posix.split('/'));
|
|
325
349
|
workflowCorePosixPaths.push({ posix, abs });
|
|
@@ -370,13 +394,13 @@ function generateUpgrade(config) {
|
|
|
370
394
|
fs.rmSync(subDir, { recursive: true, force: true });
|
|
371
395
|
}
|
|
372
396
|
}
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
397
|
+
// v0.18.2: do NOT wholesale-delete .gemini/commands. The smart-diff loop
|
|
398
|
+
// below iterates template command files individually and preserves user
|
|
399
|
+
// customizations of tracked paths (e.g. audit-merge.toml) — closing the
|
|
400
|
+
// v0.18.1 plumbing gap where these files were silently overwritten on
|
|
401
|
+
// every upgrade despite being tracked in expectedSmartDiffTrackedPaths.
|
|
402
|
+
// Custom Gemini commands (files NOT in the template) are left in place,
|
|
403
|
+
// mirroring the agent strategy from v0.16.10.
|
|
380
404
|
|
|
381
405
|
// Copy fresh from template
|
|
382
406
|
const templateToolDir = path.join(templateDir, dir);
|
|
@@ -522,16 +546,117 @@ function generateUpgrade(config) {
|
|
|
522
546
|
continue;
|
|
523
547
|
}
|
|
524
548
|
|
|
525
|
-
//
|
|
526
|
-
|
|
549
|
+
// v0.18.2: smart-diff loop for commands (closes v0.18.1 plumbing gap).
|
|
550
|
+
//
|
|
551
|
+
// Pre-v0.18.2: .claude/commands wholesale cpSync'd every file (line 525-531)
|
|
552
|
+
// and .gemini/commands wholesale rmSync'd + recursive cpSync'd (lines 374-378
|
|
553
|
+
// and the fallback at 532-533). Both branches overwrote user customizations
|
|
554
|
+
// silently despite the paths being added to expectedSmartDiffTrackedPaths in
|
|
555
|
+
// v0.18.1 (lib/meta.js:284-320).
|
|
556
|
+
//
|
|
557
|
+
// Now: iterate template command files individually. For each file:
|
|
558
|
+
// - If posix path is tracked → smart-diff decision tree (preserve customizations)
|
|
559
|
+
// - Else → unconditional overwrite (SDD-owned wrappers, e.g. add-feature.toml)
|
|
560
|
+
// Custom commands (files NOT in template) are NOT touched, mirroring the
|
|
561
|
+
// agent strategy from v0.16.10.
|
|
562
|
+
//
|
|
563
|
+
// Commands have NO project-type or stack adaptations — adapted target == raw template.
|
|
564
|
+
if (sub === 'commands') {
|
|
527
565
|
fs.mkdirSync(destSub, { recursive: true });
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
566
|
+
const trackedSet = expectedSmartDiffTrackedPaths(aiTools, projectType);
|
|
567
|
+
const templateCmdFiles = fs
|
|
568
|
+
.readdirSync(srcSub, { withFileTypes: true })
|
|
569
|
+
.filter((e) => e.isFile())
|
|
570
|
+
.map((e) => e.name);
|
|
571
|
+
|
|
572
|
+
for (const file of templateCmdFiles) {
|
|
573
|
+
const templateCmdPath = path.join(srcSub, file);
|
|
574
|
+
const existingCmdPath = path.join(destSub, file);
|
|
575
|
+
const relativePath = path.relative(dest, existingCmdPath);
|
|
576
|
+
const posixPath = toPosix(relativePath);
|
|
577
|
+
const rawTemplate = fs.readFileSync(templateCmdPath, 'utf8');
|
|
578
|
+
|
|
579
|
+
if (!trackedSet.has(posixPath)) {
|
|
580
|
+
// SDD-owned (e.g. add-feature.toml, fix-bug.toml, next-task.toml,
|
|
581
|
+
// show-progress.toml, start-task.toml) — unconditional overwrite.
|
|
582
|
+
if (fs.existsSync(existingCmdPath)) {
|
|
583
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
584
|
+
}
|
|
585
|
+
fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
|
|
586
|
+
replaced++;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// --- v0.18.2 commands smart-diff decision tree (mirror of agents) ---
|
|
591
|
+
if (!fs.existsSync(existingCmdPath)) {
|
|
592
|
+
fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
|
|
593
|
+
filesToAdapt.add(posixPath);
|
|
594
|
+
replaced++;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (config.forceTemplate) {
|
|
599
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
600
|
+
fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
|
|
601
|
+
filesToAdapt.add(posixPath);
|
|
602
|
+
replaced++;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const existingContent = fs.readFileSync(existingCmdPath, 'utf8');
|
|
607
|
+
const storedHash = meta && meta.hashes[posixPath];
|
|
608
|
+
|
|
609
|
+
const preserveCmd = () => {
|
|
610
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
611
|
+
const newBackupPath = path.join(
|
|
612
|
+
dest,
|
|
613
|
+
'.sdd-backup',
|
|
614
|
+
backupTimestamp,
|
|
615
|
+
`${relativePath}.new`
|
|
616
|
+
);
|
|
617
|
+
try {
|
|
618
|
+
fs.mkdirSync(path.dirname(newBackupPath), { recursive: true });
|
|
619
|
+
fs.writeFileSync(newBackupPath, rawTemplate, 'utf8');
|
|
620
|
+
} catch (e) {
|
|
621
|
+
console.warn(
|
|
622
|
+
` ⚠ Failed to write .new backup for ${relativePath}: ${e.code || e.message}`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
modifiedAgentsResults.push({ name: relativePath, modified: true });
|
|
626
|
+
preserved++;
|
|
627
|
+
// Codex M1 invariant: preserved files do NOT update newHashes.
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
if (storedHash) {
|
|
631
|
+
const currentHash = computeHash(existingContent);
|
|
632
|
+
if (currentHash === storedHash) {
|
|
633
|
+
// Pristine — replace + record new hash.
|
|
634
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
635
|
+
fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
|
|
636
|
+
filesToAdapt.add(posixPath);
|
|
637
|
+
replaced++;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
// Hash mismatch → user customized → preserve.
|
|
641
|
+
preserveCmd();
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Fallback — no hash (e.g. pre-v0.18.1 install). Compare against
|
|
646
|
+
// raw template (commands have no adapters).
|
|
647
|
+
if (normalizedContentEquals(existingContent, rawTemplate)) {
|
|
648
|
+
backupBeforeReplace(dest, relativePath, backupTimestamp);
|
|
649
|
+
fs.writeFileSync(existingCmdPath, rawTemplate, 'utf8');
|
|
650
|
+
filesToAdapt.add(posixPath);
|
|
651
|
+
replaced++;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
preserveCmd();
|
|
531
655
|
}
|
|
532
|
-
|
|
533
|
-
fs.cpSync(srcSub, destSub, { recursive: true });
|
|
656
|
+
continue;
|
|
534
657
|
}
|
|
658
|
+
|
|
659
|
+
fs.cpSync(srcSub, destSub, { recursive: true });
|
|
535
660
|
replaced++;
|
|
536
661
|
}
|
|
537
662
|
|
package/package.json
CHANGED
|
@@ -55,14 +55,14 @@ Eleven empirically-validated drift patterns. Failures are NOT blockers for the c
|
|
|
55
55
|
**12. P1 — PR body test count stale.** The PR body's "npm test" line should match the terminal test count in the ticket (AC / DoD / Completion Log last entry). Agents commonly open the PR at Step 4 and add tests during Step 5 review — the PR body number becomes stale.
|
|
56
56
|
```bash
|
|
57
57
|
PR_BODY=$(gh pr view --json body -q .body)
|
|
58
|
-
PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests
|
|
59
|
-
TICKET_TESTS=$(grep -iE "(npm test|tests
|
|
58
|
+
PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" | grep -oE "[0-9]+/[0-9]+" | head -1)
|
|
59
|
+
TICKET_TESTS=$(grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | tail -1)
|
|
60
60
|
[ -n "$PR_TESTS" ] && [ -n "$TICKET_TESTS" ] && [ "$PR_TESTS" != "$TICKET_TESTS" ] && flag "P1 drift: PR body $PR_TESTS vs ticket $TICKET_TESTS"
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
**13. P2 — Merge Checklist Evidence rows aspirational.** Rows marked `[x]` with future-tense Evidence ("will land", "to be created", "pending", "next commit", "TBD") — the row claims done but the work hasn't happened yet.
|
|
64
64
|
```bash
|
|
65
|
-
awk '/^## Merge Checklist Evidence
|
|
65
|
+
awk '/^## Merge Checklist Evidence/{flag=1; next} /^## /{flag=0} flag' "$TICKET" \
|
|
66
66
|
| grep -E '^\|.*\[x\].*(to be |will |pending|TBD|Will be |to be created|next commit|aspirational)' \
|
|
67
67
|
&& flag "P2 drift: aspirational row(s) found"
|
|
68
68
|
```
|
|
@@ -82,7 +82,7 @@ done < /tmp/pm_items.txt
|
|
|
82
82
|
|
|
83
83
|
**15. P4 — Remote branch orphan after "deleted".** Workflow Step 6 claims `[x] branch deleted` but origin still has the branch.
|
|
84
84
|
```bash
|
|
85
|
-
BRANCH=$(grep -
|
|
85
|
+
BRANCH=$(grep -oE '\*\*[Bb]ranch:\*\*[[:space:]]*[^[:space:]|()]+' "$TICKET" | head -1 | sed -E 's/^\*\*[Bb]ranch:\*\*[[:space:]]*//')
|
|
86
86
|
git fetch origin --prune --quiet
|
|
87
87
|
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q refs/heads && flag "P4 drift: remote branch $BRANCH still exists (run: git push origin --delete $BRANCH)"
|
|
88
88
|
```
|
|
@@ -91,7 +91,10 @@ git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q refs/heads && flag
|
|
|
91
91
|
```bash
|
|
92
92
|
FROZEN_COUNT=0
|
|
93
93
|
for t in docs/tickets/*.md; do
|
|
94
|
-
status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1
|
|
94
|
+
status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1 \
|
|
95
|
+
| sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
|
|
96
|
+
| sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
|
|
97
|
+
| sed -E 's/[[:space:]]+$//')
|
|
95
98
|
[ "$status" = "Done" ] && continue
|
|
96
99
|
ticket_id=$(basename "$t" .md | sed -E 's/-[a-z].*//')
|
|
97
100
|
git log --all --oneline --grep="$ticket_id" | grep -q . && FROZEN_COUNT=$((FROZEN_COUNT+1))
|
|
@@ -126,15 +129,15 @@ COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
|
|
|
126
129
|
CHECKED_STEPS=$(echo "$WORKFLOW" | grep -E "^- \[x\] Step [0-9]+:" | sed -E 's/^- \[x\] Step ([0-9]+):.*/\1/' | sort -u)
|
|
127
130
|
while read -r step_num; do
|
|
128
131
|
[ -z "$step_num" ] && continue
|
|
129
|
-
echo "$COMPLETION" | grep -qE "Step[[:space:]]+$step_num([^0-9]|$)" || flag "P8 drift: Step $step_num [x] but no Completion Log entry"
|
|
132
|
+
echo "$COMPLETION" | grep -qE "^\|[^|]*\|[[:space:]]*Step[[:space:]]+$step_num([^0-9]|$)" || flag "P8 drift: Step $step_num [x] but no dedicated Completion Log entry"
|
|
130
133
|
done <<< "$CHECKED_STEPS"
|
|
131
134
|
```
|
|
132
135
|
|
|
133
136
|
**20. P9 — Tracker header "Last Updated" stale.** The `**Last Updated:**` header and the `**Active Feature:**` detail should agree on step number (e.g., both say 5/6). Mismatch suggests the header wasn't refreshed after state transitions.
|
|
134
137
|
```bash
|
|
135
138
|
TRACKER=docs/project_notes/product-tracker.md
|
|
136
|
-
HEADER_STEP=$(grep -oE 'Step [0-9]+/6'
|
|
137
|
-
DETAIL_STEP=$(grep -A 1 '^\*\*Active Feature:\*\*' "$TRACKER" | grep -oE 'Step [0-9]+/6' | head -1)
|
|
139
|
+
HEADER_STEP=$(grep '^\*\*Last Updated:\*\*' "$TRACKER" | grep -oE '(Step )?[0-9]+/6' | head -1 | sed -E 's/^Step //')
|
|
140
|
+
DETAIL_STEP=$(grep -A 1 '^\*\*Active Feature:\*\*' "$TRACKER" | grep -oE '(Step )?[0-9]+/6' | head -1 | sed -E 's/^Step //')
|
|
138
141
|
[ -n "$HEADER_STEP" ] && [ -n "$DETAIL_STEP" ] && [ "$HEADER_STEP" != "$DETAIL_STEP" ] \
|
|
139
142
|
&& flag "P9 drift: tracker header says $HEADER_STEP, Active Feature says $DETAIL_STEP"
|
|
140
143
|
```
|
|
@@ -151,7 +154,10 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
|
151
154
|
|
|
152
155
|
**22. P11 — Tracker Features table status vs ticket Status mismatch.** Ticket Status=Ready for Merge / Review → tracker expects `in-progress`. Ticket Status=Done → tracker expects `done`. Mismatch means one side wasn't updated after the state change.
|
|
153
156
|
```bash
|
|
154
|
-
TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1
|
|
157
|
+
TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
|
|
158
|
+
| sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
|
|
159
|
+
| sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
|
|
160
|
+
| sed -E 's/[[:space:]]+$//')
|
|
155
161
|
FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
|
|
156
162
|
TRACKER_STATUS=$(grep -F "$FEATURE_ID" docs/project_notes/product-tracker.md | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
|
|
157
163
|
case "$TICKET_STATUS" in
|
|
@@ -163,6 +169,40 @@ esac
|
|
|
163
169
|
&& flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
|
|
164
170
|
```
|
|
165
171
|
|
|
172
|
+
**23. P12 — Tracker HEAD references stale (added v0.18.2).** The `**Last Updated:**` and `**Active Feature:**` lines may embed `HEAD <sha>` or `HEAD: <sha>` references that were correct when written but went stale as further commits landed (empirically observed in fx F-WEB-MENU-VISION-001 audit cycle 2026-05-06: tracker said `HEAD: fd752e4` while `git rev-parse HEAD` was `6fa801e` after the agent's own self-edit commit). Compare each extracted SHA against the active branch HEAD. Bidirectional prefix tolerance: a 7-char tracker SHA matches the full 40-char actual HEAD if it's a prefix; a full 40-char tracker SHA matches if its first 7 chars equal the actual short form. Scoped strictly to the two header lines so narrative SHAs in "Last Completed" prose never false-positive-fire.
|
|
173
|
+
```bash
|
|
174
|
+
TRACKER=docs/project_notes/product-tracker.md
|
|
175
|
+
if [ -f "$TRACKER" ]; then
|
|
176
|
+
ACTUAL_HEAD=$(git rev-parse HEAD 2>/dev/null || true)
|
|
177
|
+
if [ -n "$ACTUAL_HEAD" ]; then
|
|
178
|
+
ACTUAL_SHORT=$(printf '%s' "$ACTUAL_HEAD" | cut -c1-7)
|
|
179
|
+
TRACKER_HEADS=$(grep -E '^\*\*(Last Updated|Active Feature):\*\*' "$TRACKER" 2>/dev/null \
|
|
180
|
+
| grep -oE 'HEAD[[:space:]:]+`?[a-f0-9]{7,40}`?' \
|
|
181
|
+
| grep -oE '[a-f0-9]{7,40}' \
|
|
182
|
+
| sort -u || true)
|
|
183
|
+
for sha in $TRACKER_HEADS; do
|
|
184
|
+
case "$ACTUAL_HEAD" in "$sha"*) continue ;; esac
|
|
185
|
+
case "$sha" in "$ACTUAL_SHORT"*) continue ;; esac
|
|
186
|
+
flag "P12 drift: tracker HEAD reference $sha does not match git rev-parse HEAD ($ACTUAL_HEAD); refresh tracker"
|
|
187
|
+
done
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Execution discipline (added v0.18.1)
|
|
193
|
+
|
|
194
|
+
For each of the 12 drift checks (P1–P12), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
|
|
195
|
+
|
|
196
|
+
Recommended pattern:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
P1 PR body test count stale | PASS | PR_TESTS=4110/4110, TICKET_TESTS=4110/4110 (matched)
|
|
200
|
+
P2 Aspirational rows | PASS | awk … | grep … (no rows matched)
|
|
201
|
+
P5 Frozen ticket Status | PASS | FROZEN_COUNT=0
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This prevents two failure modes empirically observed during the v0.18.1 origin audit (fx F-H9 + F-H10): (a) the agent abbreviates execution and reports CLEAN by inference from MEMORY/design knowledge; (b) buggy recipes return empty output silently and the agent treats empty as PASS without verifying the recipe ran. Both are caught when literal output is required.
|
|
205
|
+
|
|
166
206
|
### Output Format
|
|
167
207
|
|
|
168
208
|
Report two tables — one for **structural (blocking)** compliance, one for **drift (advisory)**. Emit two verdicts plus a combined summary line.
|
|
@@ -188,7 +228,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
188
228
|
|
|
189
229
|
**STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
|
|
190
230
|
|
|
191
|
-
### Drift (12-
|
|
231
|
+
### Drift (12-23) — advisory, refresh before user authorization
|
|
192
232
|
|
|
193
233
|
| # | Pattern | Status | Detail |
|
|
194
234
|
|---|---------|:------:|--------|
|
|
@@ -203,6 +243,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
203
243
|
| 20 | P9 Tracker header stale | PASS | header = detail |
|
|
204
244
|
| 21 | P10 Duplicate log rows | PASS | no duplicates |
|
|
205
245
|
| 22 | P11 Tracker status mismatch | PASS | in-progress for Ready for Merge |
|
|
246
|
+
| 23 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
|
|
206
247
|
|
|
207
248
|
**DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
|
|
208
249
|
|
|
@@ -234,6 +275,7 @@ Fix them directly:
|
|
|
234
275
|
- **P9 (Tracker header stale)** → update `**Last Updated:**` line step reference to match Active Feature detail
|
|
235
276
|
- **P10 (Duplicate log rows)** → remove duplicate rows
|
|
236
277
|
- **P11 (Tracker status mismatch)** → sync tracker Features row status to ticket header Status
|
|
278
|
+
- **P12 (Tracker HEAD reference stale)** → update `**Last Updated:**` and `**Active Feature:**` `HEAD: <sha>` tokens to match `git rev-parse HEAD`. Use `git rev-parse --short HEAD` for the 7-char form.
|
|
237
279
|
|
|
238
280
|
After fixing, re-run the audit to confirm all checks pass.
|
|
239
281
|
|
|
@@ -55,48 +55,52 @@ Eleven empirically-validated drift patterns. Failures are NOT blockers for the c
|
|
|
55
55
|
**12. P1 — PR body test count stale.** Ratio must co-occur with test/pass/green marker to avoid AC/DoD ratios (14/14, 7/7).
|
|
56
56
|
```bash
|
|
57
57
|
PR_BODY=$(gh pr view --json body -q .body)
|
|
58
|
-
PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests
|
|
59
|
-
TICKET_TESTS=$(grep -iE "(npm test|tests
|
|
60
|
-
[ -n "$PR_TESTS" ] && [ -n "$TICKET_TESTS" ] && [ "$PR_TESTS" != "$TICKET_TESTS" ] && flag "P1: PR body $PR_TESTS vs ticket $TICKET_TESTS"
|
|
58
|
+
PR_TESTS=$(echo "$PR_BODY" | grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" | grep -oE "[0-9]+/[0-9]+" | head -1)
|
|
59
|
+
TICKET_TESTS=$(grep -iE "(npm test|tests?[^|]*[0-9]|[*: ]tests?[*: ]+[0-9])" "$TICKET" | grep -oE "[0-9]+/[0-9]+" | tail -1)
|
|
60
|
+
[ -n "$PR_TESTS" ] && [ -n "$TICKET_TESTS" ] && [ "$PR_TESTS" != "$TICKET_TESTS" ] && flag "P1 drift: PR body $PR_TESTS vs ticket $TICKET_TESTS"
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
**13. P2 — Merge Checklist Evidence aspirational.** `[x]` rows with future-tense text.
|
|
64
64
|
```bash
|
|
65
|
-
awk '/^## Merge Checklist Evidence
|
|
65
|
+
awk '/^## Merge Checklist Evidence/{flag=1; next} /^## /{flag=0} flag' "$TICKET" \
|
|
66
66
|
| grep -E '^\|.*\[x\].*(to be |will |pending|TBD|Will be |to be created|next commit|aspirational)' \
|
|
67
|
-
&& flag "P2: aspirational row(s)"
|
|
67
|
+
&& flag "P2 drift: aspirational row(s) found"
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
**14. P3 — Post-merge actions not logged** (post-merge only).
|
|
71
71
|
```bash
|
|
72
|
+
# Strip checkbox prefix before comparison; use grep -Fq fixed-string match.
|
|
72
73
|
grep -E "^- \[ \].*(post-merge|operator|prod rollout|pending verification)" "$TICKET" \
|
|
73
74
|
| sed -E 's/^- \[ \] //' > /tmp/pm_items.txt
|
|
74
75
|
COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
|
|
75
76
|
while IFS= read -r item; do
|
|
76
77
|
[ -z "$item" ] && continue
|
|
77
78
|
KEY=$(echo "$item" | cut -c1-40)
|
|
78
|
-
echo "$COMPLETION" | grep -Fq "$KEY" || flag "P3: '$item' not
|
|
79
|
+
echo "$COMPLETION" | grep -Fq "$KEY" || flag "P3 drift: post-merge '$item' not in Completion Log"
|
|
79
80
|
done < /tmp/pm_items.txt
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
**15. P4 — Remote branch orphan.**
|
|
83
84
|
```bash
|
|
84
|
-
BRANCH=$(grep -
|
|
85
|
+
BRANCH=$(grep -oE '\*\*[Bb]ranch:\*\*[[:space:]]*[^[:space:]|()]+' "$TICKET" | head -1 | sed -E 's/^\*\*[Bb]ranch:\*\*[[:space:]]*//')
|
|
85
86
|
git fetch origin --prune --quiet
|
|
86
|
-
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q refs/heads && flag "P4: branch $BRANCH still
|
|
87
|
+
git ls-remote --heads origin "$BRANCH" 2>/dev/null | grep -q refs/heads && flag "P4 drift: remote branch $BRANCH still exists (run: git push origin --delete $BRANCH)"
|
|
87
88
|
```
|
|
88
89
|
|
|
89
90
|
**16. P5 — Frozen ticket Status post-merge.** Multi-word status via sed char class, not `\w+`.
|
|
90
91
|
```bash
|
|
91
92
|
FROZEN_COUNT=0
|
|
92
93
|
for t in docs/tickets/*.md; do
|
|
93
|
-
status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1
|
|
94
|
+
status=$(grep -E "^\*\*Status:\*\*" "$t" | head -1 \
|
|
95
|
+
| sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
|
|
96
|
+
| sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
|
|
97
|
+
| sed -E 's/[[:space:]]+$//')
|
|
94
98
|
[ "$status" = "Done" ] && continue
|
|
95
99
|
ticket_id=$(basename "$t" .md | sed -E 's/-[a-z].*//')
|
|
96
100
|
git log --all --oneline --grep="$ticket_id" | grep -q . && FROZEN_COUNT=$((FROZEN_COUNT+1))
|
|
97
101
|
done
|
|
98
|
-
[ "$FROZEN_COUNT" -ge 2 ] && flag "P5 SYSTEMIC: $FROZEN_COUNT frozen tickets"
|
|
99
|
-
[ "$FROZEN_COUNT" -eq 1 ] && flag "P5: 1 frozen ticket"
|
|
102
|
+
[ "$FROZEN_COUNT" -ge 2 ] && flag "P5 drift (SYSTEMIC): $FROZEN_COUNT frozen tickets — Status not updated post-merge"
|
|
103
|
+
[ "$FROZEN_COUNT" -eq 1 ] && flag "P5 drift: 1 frozen ticket"
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
**17. P6 — AC count off-by-N.**
|
|
@@ -104,7 +108,7 @@ done
|
|
|
104
108
|
ACTUAL=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET" | grep -cE "^- \[[x ]\]")
|
|
105
109
|
CLAIMED=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1 | grep -oE "[0-9]+" | head -1)
|
|
106
110
|
[ -n "$CLAIMED" ] && [ "$CLAIMED" != "$ACTUAL" ] && [ $((ACTUAL - CLAIMED)) -ge 2 -o $((CLAIMED - ACTUAL)) -ge 2 ] \
|
|
107
|
-
&& flag "P6: claim $CLAIMED vs actual $ACTUAL"
|
|
111
|
+
&& flag "P6 drift: claim '$CLAIMED' vs actual AC count $ACTUAL"
|
|
108
112
|
```
|
|
109
113
|
|
|
110
114
|
**18. P7 — Test count drift within ticket (final-sections only).**
|
|
@@ -112,8 +116,9 @@ CLAIMED=$(grep -oE 'all [0-9]+ marked|AC: [0-9]+/[0-9]+' "$TICKET" | head -1 | g
|
|
|
112
116
|
TERMINAL=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET" | grep -iE "(test|pass|green)" | grep -oE "[0-9]+/[0-9]+" | tail -1)
|
|
113
117
|
AC=$(awk '/^## Acceptance Criteria/,/^## Definition of Done/' "$TICKET")
|
|
114
118
|
DOD=$(awk '/^## Definition of Done/,/^## Workflow Checklist/' "$TICKET")
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
FINAL_NUMS=$(printf '%s\n%s\n' "$AC" "$DOD" | grep -iE "(test|pass|green)" | grep -oE "[0-9]+/[0-9]+" | sort -u)
|
|
120
|
+
for n in $FINAL_NUMS; do
|
|
121
|
+
[ -n "$TERMINAL" ] && [ "$n" != "$TERMINAL" ] && flag "P7 drift: final-section count $n vs terminal $TERMINAL"
|
|
117
122
|
done
|
|
118
123
|
```
|
|
119
124
|
|
|
@@ -124,17 +129,17 @@ COMPLETION=$(awk '/^## Completion Log/,/^## Merge Checklist/' "$TICKET")
|
|
|
124
129
|
CHECKED_STEPS=$(echo "$WORKFLOW" | grep -E "^- \[x\] Step [0-9]+:" | sed -E 's/^- \[x\] Step ([0-9]+):.*/\1/' | sort -u)
|
|
125
130
|
while read -r step_num; do
|
|
126
131
|
[ -z "$step_num" ] && continue
|
|
127
|
-
echo "$COMPLETION" | grep -qE "Step[[:space:]]+$step_num([^0-9]|$)" || flag "P8: Step $step_num [x] but no
|
|
132
|
+
echo "$COMPLETION" | grep -qE "^\|[^|]*\|[[:space:]]*Step[[:space:]]+$step_num([^0-9]|$)" || flag "P8 drift: Step $step_num [x] but no dedicated Completion Log entry"
|
|
128
133
|
done <<< "$CHECKED_STEPS"
|
|
129
134
|
```
|
|
130
135
|
|
|
131
136
|
**20. P9 — Tracker header stale.**
|
|
132
137
|
```bash
|
|
133
138
|
TRACKER=docs/project_notes/product-tracker.md
|
|
134
|
-
HEADER_STEP=$(grep -oE 'Step [0-9]+/6'
|
|
135
|
-
DETAIL_STEP=$(grep -A 1 '^\*\*Active Feature:\*\*' "$TRACKER" | grep -oE 'Step [0-9]+/6' | head -1)
|
|
139
|
+
HEADER_STEP=$(grep '^\*\*Last Updated:\*\*' "$TRACKER" | grep -oE '(Step )?[0-9]+/6' | head -1 | sed -E 's/^Step //')
|
|
140
|
+
DETAIL_STEP=$(grep -A 1 '^\*\*Active Feature:\*\*' "$TRACKER" | grep -oE '(Step )?[0-9]+/6' | head -1 | sed -E 's/^Step //')
|
|
136
141
|
[ -n "$HEADER_STEP" ] && [ -n "$DETAIL_STEP" ] && [ "$HEADER_STEP" != "$DETAIL_STEP" ] \
|
|
137
|
-
&& flag "P9: header $HEADER_STEP
|
|
142
|
+
&& flag "P9 drift: tracker header says $HEADER_STEP, Active Feature says $DETAIL_STEP"
|
|
138
143
|
```
|
|
139
144
|
|
|
140
145
|
**21. P10 — Duplicate Completion Log rows.**
|
|
@@ -143,12 +148,16 @@ awk -F'|' '/^\| [0-9]{4}-[0-9]{2}-[0-9]{2}/ {
|
|
|
143
148
|
key = $2 "|" $3 "|" substr($4, 1, 80)
|
|
144
149
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)
|
|
145
150
|
print key
|
|
146
|
-
}' "$TICKET" | sort | uniq -d
|
|
151
|
+
}' "$TICKET" | sort | uniq -d \
|
|
152
|
+
| while read -r dup; do flag "P10 drift: duplicate Completion Log row: $dup"; done
|
|
147
153
|
```
|
|
148
154
|
|
|
149
155
|
**22. P11 — Tracker Features table status vs ticket Status mismatch.**
|
|
150
156
|
```bash
|
|
151
|
-
TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1
|
|
157
|
+
TICKET_STATUS=$(grep -E "^\*\*Status:\*\*" "$TICKET" | head -1 \
|
|
158
|
+
| sed -E 's/^\*\*Status:\*\*[[:space:]]*\*?\*?//' \
|
|
159
|
+
| sed -E 's/[[:space:]]*\*?\*?[[:space:]]*\|.*//' \
|
|
160
|
+
| sed -E 's/[[:space:]]+$//')
|
|
152
161
|
FEATURE_ID=$(basename "$TICKET" .md | sed -E 's/-[a-z].*//')
|
|
153
162
|
TRACKER_STATUS=$(grep -F "$FEATURE_ID" docs/project_notes/product-tracker.md | grep -oE "\| (in-progress|done|pending|blocked) \|" | head -1 | sed -E 's/\| ([a-z-]+) \|/\1/')
|
|
154
163
|
case "$TICKET_STATUS" in
|
|
@@ -157,9 +166,43 @@ case "$TICKET_STATUS" in
|
|
|
157
166
|
*) EXPECTED="" ;;
|
|
158
167
|
esac
|
|
159
168
|
[ -n "$EXPECTED" ] && [ -n "$TRACKER_STATUS" ] && [ "$TRACKER_STATUS" != "$EXPECTED" ] \
|
|
160
|
-
&& flag "P11: Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
|
|
169
|
+
&& flag "P11 drift: ticket Status='$TICKET_STATUS' expects tracker='$EXPECTED' but tracker='$TRACKER_STATUS'"
|
|
161
170
|
```
|
|
162
171
|
|
|
172
|
+
**23. P12 — Tracker HEAD references stale (added v0.18.2).** The `**Last Updated:**` and `**Active Feature:**` lines may embed `HEAD <sha>` or `HEAD: <sha>` references that were correct when written but went stale as further commits landed (empirically observed in fx F-WEB-MENU-VISION-001 audit cycle 2026-05-06: tracker said `HEAD: fd752e4` while `git rev-parse HEAD` was `6fa801e` after the agent's own self-edit commit). Compare each extracted SHA against the active branch HEAD. Bidirectional prefix tolerance: a 7-char tracker SHA matches the full 40-char actual HEAD if it's a prefix; a full 40-char tracker SHA matches if its first 7 chars equal the actual short form. Scoped strictly to the two header lines so narrative SHAs in "Last Completed" prose never false-positive-fire.
|
|
173
|
+
```bash
|
|
174
|
+
TRACKER=docs/project_notes/product-tracker.md
|
|
175
|
+
if [ -f "$TRACKER" ]; then
|
|
176
|
+
ACTUAL_HEAD=$(git rev-parse HEAD 2>/dev/null || true)
|
|
177
|
+
if [ -n "$ACTUAL_HEAD" ]; then
|
|
178
|
+
ACTUAL_SHORT=$(printf '%s' "$ACTUAL_HEAD" | cut -c1-7)
|
|
179
|
+
TRACKER_HEADS=$(grep -E '^\*\*(Last Updated|Active Feature):\*\*' "$TRACKER" 2>/dev/null \
|
|
180
|
+
| grep -oE 'HEAD[[:space:]:]+`?[a-f0-9]{7,40}`?' \
|
|
181
|
+
| grep -oE '[a-f0-9]{7,40}' \
|
|
182
|
+
| sort -u || true)
|
|
183
|
+
for sha in $TRACKER_HEADS; do
|
|
184
|
+
case "$ACTUAL_HEAD" in "$sha"*) continue ;; esac
|
|
185
|
+
case "$sha" in "$ACTUAL_SHORT"*) continue ;; esac
|
|
186
|
+
flag "P12 drift: tracker HEAD reference $sha does not match git rev-parse HEAD ($ACTUAL_HEAD); refresh tracker"
|
|
187
|
+
done
|
|
188
|
+
fi
|
|
189
|
+
fi
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Execution discipline (added v0.18.1)
|
|
193
|
+
|
|
194
|
+
For each of the 12 drift checks (P1–P12), if you declare PASS, **include the literal command output** (or its absence — explicit "no rows matched", "extracted: feature/foo", "FROZEN_COUNT=0") as evidence in your report. A bare "PASS" without supporting output is treated as **NOT EXECUTED** by the auditor — re-run with output captured.
|
|
195
|
+
|
|
196
|
+
Recommended pattern:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
P1 PR body test count stale | PASS | PR_TESTS=4110/4110, TICKET_TESTS=4110/4110 (matched)
|
|
200
|
+
P2 Aspirational rows | PASS | awk … | grep … (no rows matched)
|
|
201
|
+
P5 Frozen ticket Status | PASS | FROZEN_COUNT=0
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This prevents two failure modes empirically observed during the v0.18.1 origin audit (fx F-H9 + F-H10): (a) the agent abbreviates execution and reports CLEAN by inference from MEMORY/design knowledge; (b) buggy recipes return empty output silently and the agent treats empty as PASS without verifying the recipe ran. Both are caught when literal output is required.
|
|
205
|
+
|
|
163
206
|
### Output Format
|
|
164
207
|
|
|
165
208
|
Report two tables — one for **structural (blocking)** compliance, one for **drift (advisory)**. Emit two verdicts plus a combined summary line.
|
|
@@ -185,7 +228,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
185
228
|
|
|
186
229
|
**STRUCTURAL: READY FOR MERGE** (or **STRUCTURAL: NEEDS FIX — N blockers**)
|
|
187
230
|
|
|
188
|
-
### Drift (12-
|
|
231
|
+
### Drift (12-23) — advisory, refresh before user authorization
|
|
189
232
|
|
|
190
233
|
| # | Pattern | Status | Detail |
|
|
191
234
|
|---|---------|:------:|--------|
|
|
@@ -200,6 +243,7 @@ Report two tables — one for **structural (blocking)** compliance, one for **dr
|
|
|
200
243
|
| 20 | P9 Tracker header stale | PASS | header = detail |
|
|
201
244
|
| 21 | P10 Duplicate log rows | PASS | no duplicates |
|
|
202
245
|
| 22 | P11 Tracker status mismatch | PASS | status consistent |
|
|
246
|
+
| 23 | P12 Tracker HEAD reference | PASS | tracker HEAD = git HEAD |
|
|
203
247
|
|
|
204
248
|
**DRIFT: CLEAN** (or **DRIFT: N advisories — refresh before merge**)
|
|
205
249
|
|
|
@@ -231,6 +275,7 @@ Fix them directly:
|
|
|
231
275
|
- **P9** → refresh `**Last Updated:**` step reference
|
|
232
276
|
- **P10** → remove duplicate rows
|
|
233
277
|
- **P11** → sync tracker Features row status to ticket header Status
|
|
278
|
+
- **P12 (Tracker HEAD reference stale)** → update `**Last Updated:**` and `**Active Feature:**` `HEAD: <sha>` tokens to match `git rev-parse HEAD`. Use `git rev-parse --short HEAD` for the 7-char form.
|
|
234
279
|
|
|
235
280
|
After fixing, re-run the audit to confirm all checks pass.
|
|
236
281
|
|