@event4u/agent-config 8.4.0 → 8.4.1
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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +6 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +1 -1
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +1 -1
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +1 -1
- package/dist/mcp/registry-manifest.json +1 -1
- package/package.json +4 -1
- package/src/agent-src/README.md +64 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +497 -0
- package/src/agent-src/scripts/scan-seeder-violations.php +145 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +1087 -0
- package/src/agent-src/templates/AGENTS.md +34 -0
- package/src/agent-src/templates/scripts/README.md +195 -0
- package/src/agent-src/templates/scripts/check_memory.ts +662 -0
- package/src/agent-src/templates/scripts/check_memory_proposal.ts +370 -0
- package/src/agent-src/templates/scripts/historical-bug-patterns.example.yml +84 -0
- package/src/agent-src/templates/scripts/implement_ticket/__main__.ts +27 -0
- package/src/agent-src/templates/scripts/memory_hash.ts +355 -0
- package/src/agent-src/templates/scripts/memory_lookup.ts +1089 -0
- package/src/agent-src/templates/scripts/memory_report.ts +868 -0
- package/src/agent-src/templates/scripts/memory_signal.ts +444 -0
- package/src/agent-src/templates/scripts/memory_status.ts +215 -0
- package/src/agent-src/templates/scripts/ownership-map.example.yml +87 -0
- package/src/agent-src/templates/scripts/pr-risk-config.example.yml +76 -0
- package/src/agent-src/templates/scripts/pr_review_routing.ts +544 -0
- package/src/agent-src/templates/scripts/pr_risk_review.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/aggregator.ts +0 -0
- package/src/agent-src/templates/scripts/telemetry/boundary.ts +164 -0
- package/src/agent-src/templates/scripts/telemetry/engagement.ts +479 -0
- package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +394 -0
- package/src/agent-src/templates/scripts/telemetry/settings.ts +210 -0
- package/src/agent-src/templates/scripts/telemetry_record.ts +276 -0
- package/src/agent-src/templates/scripts/telemetry_report.ts +211 -0
- package/src/agent-src/templates/scripts/telemetry_status.ts +333 -0
- package/src/agent-src/templates/scripts/tier_usage_report.ts +619 -0
- package/src/agent-src/templates/scripts/work_engine/__main__.ts +14 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +1261 -0
- package/src/agent-src/templates/scripts/work_engine/_lib/user_global_paths.ts +329 -0
- package/src/agent-src/templates/scripts/work_engine/cli.ts +206 -0
- package/src/agent-src/templates/scripts/work_engine/cli_args.ts +249 -0
- package/src/agent-src/templates/scripts/work_engine/delivery_state.ts +225 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/analyze.ts +125 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/memory.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/plan.ts +267 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/refine.ts +518 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/report.ts +379 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +268 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +258 -0
- package/src/agent-src/templates/scripts/work_engine/directives/index.ts +32 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/contract.ts +243 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/index.ts +108 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +259 -0
- package/src/agent-src/templates/scripts/work_engine/directives/mixed/ui.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.ts +40 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +241 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +216 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +506 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +325 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/index.ts +102 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/polish.ts +462 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/review.ts +474 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +352 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.ts +33 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.ts +213 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/index.ts +111 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.ts +126 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/report.ts +112 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui_trivial/test.ts +164 -0
- package/src/agent-src/templates/scripts/work_engine/dispatcher.ts +515 -0
- package/src/agent-src/templates/scripts/work_engine/emitters.ts +119 -0
- package/src/agent-src/templates/scripts/work_engine/errors.ts +24 -0
- package/src/agent-src/templates/scripts/work_engine/hook_bootstrap.ts +104 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.ts +189 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.ts +41 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.ts +89 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_gate.ts +193 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +304 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.ts +110 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.ts +118 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/index.ts +17 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +161 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.ts +45 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/builtin/trace.ts +134 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/context.ts +94 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/events.ts +58 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/exceptions.ts +85 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/index.ts +27 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/registry.ts +66 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/runner.ts +90 -0
- package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/input_builders.ts +260 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/migration/v0_to_v1.ts +531 -0
- package/src/agent-src/templates/scripts/work_engine/orchestration.ts +366 -0
- package/src/agent-src/templates/scripts/work_engine/persona_policy.ts +97 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/diff.ts +135 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/file.ts +175 -0
- package/src/agent-src/templates/scripts/work_engine/resolvers/prompt.ts +115 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/confidence.ts +415 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +466 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/decision_trace.ts +298 -0
- package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +444 -0
- package/src/agent-src/templates/scripts/work_engine/stack/detect.ts +252 -0
- package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +745 -0
- package/src/agent-src/templates/scripts/work_engine/state.ts +1151 -0
- package/src/agent-src/templates/scripts/work_engine/state_io.ts +413 -0
- package/src/install/atomic.ts +111 -0
- package/src/install/conflict.ts +226 -0
- package/src/install/detect.ts +280 -0
- package/src/install/paths.ts +90 -0
- package/src/install/plan.ts +205 -0
- package/src/install/preflight.ts +210 -0
- package/src/install/selectedTools.ts +52 -0
- package/src/install/toolDetection.ts +120 -0
- package/src/install/txlog.ts +161 -0
- package/src/install/types.ts +175 -0
- package/src/install/wizard-plan.ts +216 -0
- package/src/scripts/prepack-check.mjs +108 -5
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Archive completed roadmaps — the PR-gate (council 2026-06-14).
|
|
4
|
+
*
|
|
5
|
+
* TypeScript twin of `src/agent-src/scripts/archive_completed_roadmaps.py`
|
|
6
|
+
* (ADR-200, Python→TypeScript migration). The CLI contract is mirrored EXACTLY
|
|
7
|
+
* — the `--all` / `--base` / `--dry-run` flags, the argparse usage / error text
|
|
8
|
+
* (`-h`/`--help` → exit 0, unknown arg → exit 2), the `git`-shell-out semantics
|
|
9
|
+
* (argv / cwd / returncode / stdout / stderr captured via `spawnSync`, matching
|
|
10
|
+
* `subprocess.run(..., capture_output=True, text=True)`), byte-identical stdout
|
|
11
|
+
* (the `ℹ️` / `✅` summary) and stderr (the `⚠️` warnings), the
|
|
12
|
+
* `urp.collect()` completion criterion, and exit code 0. snake_case kept.
|
|
13
|
+
*
|
|
14
|
+
* A roadmap that has reached `count_open == 0` and `count_deferred == 0` is
|
|
15
|
+
* **complete**. This sweep moves it to `agents/roadmaps/archive/`, rewrites
|
|
16
|
+
* inbound references (`agents/roadmaps/<x>.md` → `agents/roadmaps/archive/<x>.md`)
|
|
17
|
+
* across tracked files so links never break, and regenerates the dashboard.
|
|
18
|
+
*
|
|
19
|
+
* It replaces the old **merge-gate** (keep one item open + a manual post-merge
|
|
20
|
+
* archival step that got forgotten — leaving finished roadmaps to rot in `main`)
|
|
21
|
+
* with a deterministic **PR-gate**: `/create-pr` runs this before the PR is
|
|
22
|
+
* created, so the roadmap lands already-archived in the PR and merges clean.
|
|
23
|
+
*
|
|
24
|
+
* Default `--changed-only`: only archive roadmaps that appear in this branch's
|
|
25
|
+
* history since it diverged from `origin/main` (`git log origin/main..HEAD`),
|
|
26
|
+
* so a PR archives exactly the roadmaps it completed — never an unrelated complete
|
|
27
|
+
* roadmap. `--all` archives every complete active roadmap. No agent-set
|
|
28
|
+
* annotation is required — completion is detected from the checkbox counts.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ./scripts-run src/scripts/archive_completed_roadmaps # --changed-only (default)
|
|
32
|
+
* ./scripts-run src/scripts/archive_completed_roadmaps --all
|
|
33
|
+
* ./scripts-run src/scripts/archive_completed_roadmaps --base origin/main --dry-run
|
|
34
|
+
*
|
|
35
|
+
* --- Parity notes (ADR-200) ---
|
|
36
|
+
*
|
|
37
|
+
* - `subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)` →
|
|
38
|
+
* `spawnSync(cmd[0], cmd.slice(1), { cwd, encoding: 'utf-8' })`, wrapped in a
|
|
39
|
+
* `CompletedProcess`-shaped object. A spawn that fails to launch surfaces as
|
|
40
|
+
* `returncode = -1` with the error message on `stderr` (the Python path would
|
|
41
|
+
* raise; no test exercises a missing-git environment).
|
|
42
|
+
* - The Python `sys.path.insert(0, …); import update_roadmap_progress as urp`
|
|
43
|
+
* becomes an eager static import of the `.ts` twin — same `collect()`
|
|
44
|
+
* surface, no observable difference.
|
|
45
|
+
* - `_regen_dashboard` runs `[sys.executable, str(script)]`; the twin spawns
|
|
46
|
+
* the dashboard twin via the same `tsx` runtime so the regen + `git add`
|
|
47
|
+
* side effects match. It only fires on a non-dry-run with archived roadmaps.
|
|
48
|
+
* - JSON is not emitted. `set[str]` (touched paths) → `Set<string>`; iteration
|
|
49
|
+
* order is never rendered, so the unordered semantics match.
|
|
50
|
+
* - `process.exitCode` is set; `process.exit()` is never called. argparse
|
|
51
|
+
* usage errors throw `ArgparseExit(2)`; `-h`/`--help` throws `ArgparseExit(0)`.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { spawnSync } from 'node:child_process';
|
|
55
|
+
import * as fs from 'node:fs';
|
|
56
|
+
import * as path from 'node:path';
|
|
57
|
+
import process from 'node:process';
|
|
58
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
59
|
+
|
|
60
|
+
import { collect } from './update_roadmap_progress.js';
|
|
61
|
+
|
|
62
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
63
|
+
|
|
64
|
+
interface CompletedProcess {
|
|
65
|
+
returncode: number;
|
|
66
|
+
stdout: string;
|
|
67
|
+
stderr: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class ArgparseExit extends Error {
|
|
71
|
+
code: number;
|
|
72
|
+
constructor(code: number) {
|
|
73
|
+
super(`ArgparseExit(${code})`);
|
|
74
|
+
this.name = 'ArgparseExit';
|
|
75
|
+
this.code = code;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** subprocess.run(cmd, cwd=cwd, capture_output=True, text=True). */
|
|
80
|
+
function _run(cmd: string[], cwd: string): CompletedProcess {
|
|
81
|
+
const res = spawnSync(cmd[0] as string, cmd.slice(1), { cwd, encoding: 'utf-8' });
|
|
82
|
+
if (res.error) {
|
|
83
|
+
return { returncode: -1, stdout: res.stdout ?? '', stderr: String(res.error.message ?? '') };
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
returncode: res.status === null ? -1 : res.status,
|
|
87
|
+
stdout: res.stdout ?? '',
|
|
88
|
+
stderr: res.stderr ?? '',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _repo_root(): string {
|
|
93
|
+
const cp = _run(['git', 'rev-parse', '--show-toplevel'], process.cwd());
|
|
94
|
+
return cp.returncode === 0 ? cp.stdout.trim() : process.cwd();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** `str.splitlines()` over LF/CRLF/CR (subprocess text output). */
|
|
98
|
+
function _splitlines(s: string): string[] {
|
|
99
|
+
if (s === '') return [];
|
|
100
|
+
const parts = s.split(/\r\n|\r|\n/);
|
|
101
|
+
if (parts.length && parts[parts.length - 1] === '') {
|
|
102
|
+
parts.pop();
|
|
103
|
+
}
|
|
104
|
+
return parts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Repo-relative paths touched in any commit since divergence from base.
|
|
109
|
+
*
|
|
110
|
+
* Returns null when the base ref is unavailable — callers treat null as
|
|
111
|
+
* "cannot scope, fall back to --all".
|
|
112
|
+
*/
|
|
113
|
+
function _branch_touched_paths(root: string, base: string): Set<string> | null {
|
|
114
|
+
const cp = _run(
|
|
115
|
+
['git', 'log', `${base}..HEAD`, '--name-only', '--pretty=format:'],
|
|
116
|
+
root,
|
|
117
|
+
);
|
|
118
|
+
if (cp.returncode !== 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const out = new Set<string>();
|
|
122
|
+
for (const line of _splitlines(cp.stdout)) {
|
|
123
|
+
const t = line.trim();
|
|
124
|
+
if (t) {
|
|
125
|
+
out.add(t);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Rewrite full-path references `old_rel` → `new_rel` in tracked files.
|
|
133
|
+
*
|
|
134
|
+
* Only the exact repo-relative path is rewritten (bare-filename mentions are
|
|
135
|
+
* left alone). The archived file's own path never matches because the search
|
|
136
|
+
* string is the un-archived path.
|
|
137
|
+
*/
|
|
138
|
+
function _inbound_ref_rewrite(
|
|
139
|
+
root: string,
|
|
140
|
+
old_rel: string,
|
|
141
|
+
new_rel: string,
|
|
142
|
+
dry_run: boolean,
|
|
143
|
+
tracked = true,
|
|
144
|
+
): string[] {
|
|
145
|
+
const changed: string[] = [];
|
|
146
|
+
// Tracked repo → `git grep` (fast, index-aware). Untracked fallback (gap A)
|
|
147
|
+
// → walk the filesystem, since `git grep` only sees committed content and
|
|
148
|
+
// would miss every inbound ref in a pre-first-commit consumer.
|
|
149
|
+
let candidates: string[];
|
|
150
|
+
if (tracked) {
|
|
151
|
+
const grep = _run(['git', 'grep', '-l', '--', old_rel], root);
|
|
152
|
+
if (grep.returncode === 1) {
|
|
153
|
+
return changed; // no matches
|
|
154
|
+
}
|
|
155
|
+
candidates =
|
|
156
|
+
grep.returncode === 0
|
|
157
|
+
? _splitlines(grep.stdout).map((l) => l.trim())
|
|
158
|
+
: _fs_grep_files(root, old_rel); // git grep unusable → fs walk
|
|
159
|
+
} else {
|
|
160
|
+
candidates = _fs_grep_files(root, old_rel);
|
|
161
|
+
}
|
|
162
|
+
for (const line of candidates) {
|
|
163
|
+
const rel = line.trim();
|
|
164
|
+
if (!rel || rel === old_rel) {
|
|
165
|
+
// skip the roadmap file itself
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const fp = path.join(root, rel);
|
|
169
|
+
let text: string;
|
|
170
|
+
try {
|
|
171
|
+
text = fs.readFileSync(fp, { encoding: 'utf-8' });
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!text.includes(old_rel)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (!dry_run) {
|
|
179
|
+
fs.writeFileSync(fp, _replaceAll(text, old_rel, new_rel), { encoding: 'utf-8' });
|
|
180
|
+
}
|
|
181
|
+
changed.push(rel);
|
|
182
|
+
}
|
|
183
|
+
return changed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** str.replace(old, new) — replace every occurrence (Python str.replace). */
|
|
187
|
+
function _replaceAll(text: string, oldStr: string, newStr: string): string {
|
|
188
|
+
return text.split(oldStr).join(newStr);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface MoveResult {
|
|
192
|
+
/** the move (or dry-run no-op) succeeded */
|
|
193
|
+
ok: boolean;
|
|
194
|
+
/** the move went through `git mv` (tracked); false → plain filesystem
|
|
195
|
+
* rename was used because the repo has no commits / the file is untracked.
|
|
196
|
+
* The caller rewrites inbound refs on the filesystem (not via `git add`)
|
|
197
|
+
* when this is false. */
|
|
198
|
+
tracked: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Move `src_rel` → `dst_rel`, preferring `git mv`.
|
|
203
|
+
*
|
|
204
|
+
* Untracked-safe (road-to-roadmap-archival-robustness, gap A): a consumer that
|
|
205
|
+
* closed a roadmap in a pre-first-commit / untracked working tree cannot use
|
|
206
|
+
* `git mv` (it errors). Instead of printing `git mv failed` and leaving the
|
|
207
|
+
* completed roadmap to rot in the active tree, fall back to a plain
|
|
208
|
+
* `fs.renameSync`. The TS-only enhancement post-dates the deleted Python twin
|
|
209
|
+
* (ADR-200), so there is no byte-parity obligation on this failure path.
|
|
210
|
+
*/
|
|
211
|
+
function _git_mv(root: string, src_rel: string, dst_rel: string, dry_run: boolean): MoveResult {
|
|
212
|
+
const dst = path.join(root, dst_rel);
|
|
213
|
+
if (dry_run) {
|
|
214
|
+
return { ok: true, tracked: true };
|
|
215
|
+
}
|
|
216
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
217
|
+
const cp = _run(['git', 'mv', src_rel, dst_rel], root);
|
|
218
|
+
if (cp.returncode === 0) {
|
|
219
|
+
return { ok: true, tracked: true };
|
|
220
|
+
}
|
|
221
|
+
// Fallback: untracked file or a repo with no commits — plain rename.
|
|
222
|
+
try {
|
|
223
|
+
fs.renameSync(path.join(root, src_rel), dst);
|
|
224
|
+
return { ok: true, tracked: false };
|
|
225
|
+
} catch {
|
|
226
|
+
return { ok: false, tracked: false };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Repo-relative paths of files containing `needle`, by walking the filesystem.
|
|
232
|
+
*
|
|
233
|
+
* The untracked fallback for `_inbound_ref_rewrite`: `git grep` only sees
|
|
234
|
+
* tracked content, so in a pre-first-commit consumer it misses every inbound
|
|
235
|
+
* reference. Walk the tree (skipping `.git` / `node_modules` and obvious binary
|
|
236
|
+
* trees), read text files, and match the literal path. Bounded by skipping the
|
|
237
|
+
* heavy dirs; an untracked consumer's tree is small.
|
|
238
|
+
*/
|
|
239
|
+
function _fs_grep_files(root: string, needle: string): string[] {
|
|
240
|
+
const SKIP = new Set(['.git', 'node_modules', '.augment', 'dist']);
|
|
241
|
+
const hits: string[] = [];
|
|
242
|
+
const walk = (dirAbs: string): void => {
|
|
243
|
+
let entries: fs.Dirent[];
|
|
244
|
+
try {
|
|
245
|
+
entries = fs.readdirSync(dirAbs, { withFileTypes: true });
|
|
246
|
+
} catch {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
for (const e of entries) {
|
|
250
|
+
if (e.name.startsWith('.git') || SKIP.has(e.name)) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const abs = path.join(dirAbs, e.name);
|
|
254
|
+
if (e.isDirectory()) {
|
|
255
|
+
walk(abs);
|
|
256
|
+
} else if (e.isFile() && /\.(md|json|ya?ml|ts|js|txt)$/.test(e.name)) {
|
|
257
|
+
let text: string;
|
|
258
|
+
try {
|
|
259
|
+
text = fs.readFileSync(abs, { encoding: 'utf-8' });
|
|
260
|
+
} catch {
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (text.includes(needle)) {
|
|
264
|
+
hits.push(path.relative(root, abs).split(path.sep).join('/'));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
walk(root);
|
|
270
|
+
return hits;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface ArchiveRecord {
|
|
274
|
+
roadmap: string;
|
|
275
|
+
archived_to: string;
|
|
276
|
+
refs_migrated: string[];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Archive every complete active roadmap (count_open==0, count_deferred==0). */
|
|
280
|
+
function archive_completed(
|
|
281
|
+
root: string,
|
|
282
|
+
opts: { changed_only: boolean; base: string; dry_run: boolean },
|
|
283
|
+
): ArchiveRecord[] {
|
|
284
|
+
const { changed_only, base, dry_run } = opts;
|
|
285
|
+
const roadmap_root = path.join(root, 'agents', 'roadmaps');
|
|
286
|
+
if (!_isDir(roadmap_root)) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
const touched = changed_only ? _branch_touched_paths(root, base) : null;
|
|
290
|
+
// changed_only requested but base unavailable → conservative: archive nothing.
|
|
291
|
+
if (changed_only && touched === null) {
|
|
292
|
+
process.stderr.write(
|
|
293
|
+
` ⚠️ cannot resolve \`${base}\` — skipping the changed-only ` +
|
|
294
|
+
'archival sweep (run with --all to force).\n',
|
|
295
|
+
);
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const archived: ArchiveRecord[] = [];
|
|
300
|
+
for (const stats of collect(roadmap_root)) {
|
|
301
|
+
if (stats.open_ !== 0 || stats.deferred !== 0) {
|
|
302
|
+
continue; // not complete
|
|
303
|
+
}
|
|
304
|
+
const old_rel = `agents/roadmaps/${stats.rel}`;
|
|
305
|
+
if (changed_only && !(touched as Set<string>).has(old_rel)) {
|
|
306
|
+
continue; // complete, but not this branch's work
|
|
307
|
+
}
|
|
308
|
+
const new_rel = `agents/roadmaps/archive/${stats.rel}`;
|
|
309
|
+
const mv = _git_mv(root, old_rel, new_rel, dry_run);
|
|
310
|
+
if (!mv.ok) {
|
|
311
|
+
process.stderr.write(` ⚠️ could not archive ${old_rel} (mv failed)\n`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const refs = _inbound_ref_rewrite(root, old_rel, new_rel, dry_run, mv.tracked);
|
|
315
|
+
// Stage rewritten refs only in a tracked repo; an untracked consumer's
|
|
316
|
+
// files are not in the index, so the filesystem rewrite is the whole job.
|
|
317
|
+
if (!dry_run && refs.length && mv.tracked) {
|
|
318
|
+
_run(['git', 'add', '--', ...refs], root);
|
|
319
|
+
}
|
|
320
|
+
archived.push({ roadmap: old_rel, archived_to: new_rel, refs_migrated: refs });
|
|
321
|
+
}
|
|
322
|
+
return archived;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function _isDir(p: string): boolean {
|
|
326
|
+
try {
|
|
327
|
+
return fs.statSync(p).isDirectory();
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function _isFile(p: string): boolean {
|
|
334
|
+
try {
|
|
335
|
+
return fs.statSync(p).isFile();
|
|
336
|
+
} catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function _regen_dashboard(root: string, dry_run: boolean): void {
|
|
342
|
+
if (dry_run) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const script = path.join(path.dirname(_HERE), 'update_roadmap_progress.ts');
|
|
346
|
+
// Python: `_run([sys.executable, str(script)], root)`; the twin spawns the
|
|
347
|
+
// dashboard generator twin through the same `tsx` runtime, cwd=root.
|
|
348
|
+
_runTwin(root, script);
|
|
349
|
+
const dash = path.join(root, 'agents', 'roadmaps-progress.md');
|
|
350
|
+
if (_isFile(dash)) {
|
|
351
|
+
_run(['git', 'add', '--', 'agents/roadmaps-progress.md'], root);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Spawn the dashboard generator twin (tsx) with cwd=root, mirroring `[sys.executable, script]`. */
|
|
356
|
+
function _runTwin(root: string, script: string): void {
|
|
357
|
+
// Python uses `sys.executable` — the interpreter already running this
|
|
358
|
+
// script, which is always present. The TS analog is the `tsx` that owns
|
|
359
|
+
// THIS twin, found by walking up from the script's own dir (matching the
|
|
360
|
+
// canonical `run.ts` resolver), NOT the `git rev-parse` consumer-repo root
|
|
361
|
+
// (which need not carry a node_modules). Fall back to `npx tsx`.
|
|
362
|
+
const binName = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
|
|
363
|
+
let dir = path.dirname(_HERE);
|
|
364
|
+
for (;;) {
|
|
365
|
+
const candidate = path.join(dir, 'node_modules', '.bin', binName);
|
|
366
|
+
if (fs.existsSync(candidate)) {
|
|
367
|
+
spawnSync(candidate, [script], { cwd: root, encoding: 'utf-8' });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const parent = path.dirname(dir);
|
|
371
|
+
if (parent === dir) {
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
dir = parent;
|
|
375
|
+
}
|
|
376
|
+
spawnSync('npx', ['tsx', script], { cwd: root, encoding: 'utf-8' });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const _PROG = 'archive_completed_roadmaps.py';
|
|
380
|
+
|
|
381
|
+
function _usage(): string {
|
|
382
|
+
return `usage: ${_PROG} [-h] [--all] [--base BASE] [--dry-run]\n`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
interface Args {
|
|
386
|
+
all: boolean;
|
|
387
|
+
base: string;
|
|
388
|
+
dry_run: boolean;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function _parseArgs(argv: readonly string[]): Args {
|
|
392
|
+
let all = false;
|
|
393
|
+
let base = 'origin/main';
|
|
394
|
+
let dry_run = false;
|
|
395
|
+
const emitError = (msg: string): never => {
|
|
396
|
+
process.stderr.write(_usage());
|
|
397
|
+
process.stderr.write(`${_PROG}: error: ${msg}\n`);
|
|
398
|
+
throw new ArgparseExit(2);
|
|
399
|
+
};
|
|
400
|
+
let i = 0;
|
|
401
|
+
while (i < argv.length) {
|
|
402
|
+
const tok = argv[i] as string;
|
|
403
|
+
if (tok === '-h' || tok === '--help') {
|
|
404
|
+
process.stdout.write(_usage());
|
|
405
|
+
throw new ArgparseExit(0);
|
|
406
|
+
} else if (tok === '--all') {
|
|
407
|
+
all = true;
|
|
408
|
+
i += 1;
|
|
409
|
+
} else if (tok === '--dry-run') {
|
|
410
|
+
dry_run = true;
|
|
411
|
+
i += 1;
|
|
412
|
+
} else if (tok === '--base') {
|
|
413
|
+
const val = argv[i + 1];
|
|
414
|
+
if (val === undefined) {
|
|
415
|
+
emitError('argument --base: expected one argument');
|
|
416
|
+
}
|
|
417
|
+
base = val as string;
|
|
418
|
+
i += 2;
|
|
419
|
+
} else if (tok.startsWith('--base=')) {
|
|
420
|
+
base = tok.slice('--base='.length);
|
|
421
|
+
i += 1;
|
|
422
|
+
} else {
|
|
423
|
+
emitError(`unrecognized arguments: ${tok}`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return { all, base, dry_run };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function main(argv?: readonly string[]): number {
|
|
430
|
+
const ns = _parseArgs(argv ?? process.argv.slice(2));
|
|
431
|
+
|
|
432
|
+
const root = _repo_root();
|
|
433
|
+
const archived = archive_completed(root, {
|
|
434
|
+
changed_only: !ns.all,
|
|
435
|
+
base: ns.base,
|
|
436
|
+
dry_run: ns.dry_run,
|
|
437
|
+
});
|
|
438
|
+
if (archived.length === 0) {
|
|
439
|
+
process.stdout.write(' ℹ️ No completed roadmaps to archive.\n');
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
_regen_dashboard(root, ns.dry_run);
|
|
443
|
+
const verb = ns.dry_run ? 'Would archive' : 'Archived';
|
|
444
|
+
for (const rec of archived) {
|
|
445
|
+
process.stdout.write(
|
|
446
|
+
` ✅ ${verb}: ${rec.roadmap} → ${rec.archived_to}` +
|
|
447
|
+
(rec.refs_migrated.length
|
|
448
|
+
? ` (${rec.refs_migrated.length} ref(s) migrated)`
|
|
449
|
+
: '') +
|
|
450
|
+
'\n',
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function _isCliEntry(): boolean {
|
|
457
|
+
if (process.argv[1] === undefined) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
461
|
+
if (import.meta.url === argvUrl) {
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
// A symlinked invocation (e.g. via `.augment/scripts` → `dist/agent-src/scripts`,
|
|
465
|
+
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
466
|
+
// import.meta.url is the resolved real path while argv[1] keeps the symlink
|
|
467
|
+
// path. Compare realpaths so the entry guard still fires (without this the
|
|
468
|
+
// archival sweep silently no-ops when run through the symlink).
|
|
469
|
+
try {
|
|
470
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
471
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
472
|
+
return here === argv;
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
478
|
+
try {
|
|
479
|
+
process.exitCode = main();
|
|
480
|
+
} catch (exc) {
|
|
481
|
+
if (exc instanceof ArgparseExit) {
|
|
482
|
+
process.exitCode = exc.code;
|
|
483
|
+
} else {
|
|
484
|
+
throw exc;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export {
|
|
490
|
+
archive_completed,
|
|
491
|
+
main,
|
|
492
|
+
_branch_touched_paths,
|
|
493
|
+
_inbound_ref_rewrite,
|
|
494
|
+
_git_mv,
|
|
495
|
+
_repo_root,
|
|
496
|
+
};
|
|
497
|
+
export type { ArchiveRecord, CompletedProcess };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scans all PhpDataSeeder data files for foreign key references that use raw seeder constants
|
|
7
|
+
* instead of getReference(). Raw constants don't trigger lazy initialization of the referenced
|
|
8
|
+
* seeder, causing "items not available" errors when seeders run in unpredictable order.
|
|
9
|
+
*
|
|
10
|
+
* Usage (inside Docker container):
|
|
11
|
+
* php .augment/scripts/scan-seeder-violations.php
|
|
12
|
+
*
|
|
13
|
+
* Used by: /fix-seeder command
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
$seedersDir = __DIR__ . '/../../database/seeders';
|
|
17
|
+
$dataDir = __DIR__ . '/../../database/seeder-data/data';
|
|
18
|
+
|
|
19
|
+
if (!is_dir($seedersDir) || !is_dir($dataDir)) {
|
|
20
|
+
echo "Error: seeders or data directory not found. Run from project root.\n";
|
|
21
|
+
exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Step 1: Build map of seeder short class name -> data file relative path
|
|
25
|
+
$seederMap = [];
|
|
26
|
+
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($seedersDir));
|
|
27
|
+
|
|
28
|
+
foreach ($iterator as $file) {
|
|
29
|
+
if ($file->getExtension() !== 'php') {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$content = file_get_contents($file->getRealPath());
|
|
34
|
+
|
|
35
|
+
if (preg_match('/class\s+(\w+)\s+extends\s+(?:Php|Json)DataSeeder/', $content, $classMatch)
|
|
36
|
+
&& preg_match('/\$dataFile\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $dataMatch)) {
|
|
37
|
+
$seederMap[$classMatch[1]] = $dataMatch[1];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Known exceptions: circular dependencies where getReference() would cause infinite loops.
|
|
42
|
+
// Format: 'OwnerSeeder' => ['ReferencedSeeder', ...]
|
|
43
|
+
// Currently empty — all circular dependencies have been resolved via two-phase seeding
|
|
44
|
+
// (e.g., UserWageTypeRuleSeeder uses run() to apply project mappings after initReferences).
|
|
45
|
+
$exceptions = [];
|
|
46
|
+
|
|
47
|
+
// Step 2: For each data file, find violations
|
|
48
|
+
$violations = [];
|
|
49
|
+
|
|
50
|
+
foreach ($seederMap as $ownerSeeder => $dataFile) {
|
|
51
|
+
$filePath = $dataDir . '/' . str_replace('\\', '/', $dataFile);
|
|
52
|
+
|
|
53
|
+
if (!file_exists($filePath)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
$lines = file($filePath);
|
|
58
|
+
|
|
59
|
+
if (!$lines) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$fileExtension = pathinfo($filePath, PATHINFO_EXTENSION);
|
|
64
|
+
|
|
65
|
+
foreach ($lines as $lineNum => $line) {
|
|
66
|
+
if (!preg_match_all('/(\w+Seeder)::([A-Z][A-Z0-9_]+)/', $line, $matches, PREG_SET_ORDER)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
foreach ($matches as $match) {
|
|
71
|
+
$referencedSeeder = $match[1];
|
|
72
|
+
$constant = $match[2];
|
|
73
|
+
|
|
74
|
+
// Skip own seeder constants (primary keys are OK)
|
|
75
|
+
if ($referencedSeeder === $ownerSeeder) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Skip known exceptions (circular dependencies)
|
|
80
|
+
if (isset($exceptions[$ownerSeeder]) && in_array($referencedSeeder, $exceptions[$ownerSeeder], true)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Skip if the line already contains getReference/getReferences for this seeder
|
|
85
|
+
if (preg_match('/' . preg_quote($referencedSeeder, '/') . '::getReference/', $line)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Check multiline: previous line may have getReference(
|
|
90
|
+
if ($lineNum > 0) {
|
|
91
|
+
$prevLine = $lines[$lineNum - 1];
|
|
92
|
+
if (preg_match('/' . preg_quote($referencedSeeder, '/') . '::getReference\s*\(\s*$/', trim($prevLine))) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
$violations[] = [
|
|
98
|
+
'file' => str_replace($dataDir . '/', '', $filePath),
|
|
99
|
+
'line' => $lineNum + 1,
|
|
100
|
+
'owner' => $ownerSeeder,
|
|
101
|
+
'referenced' => $referencedSeeder,
|
|
102
|
+
'constant' => $constant,
|
|
103
|
+
'full' => $referencedSeeder . '::' . $constant,
|
|
104
|
+
'lineContent' => trim($line),
|
|
105
|
+
'extension' => $fileExtension,
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step 3: Output results
|
|
112
|
+
if (empty($violations)) {
|
|
113
|
+
echo "✅ No violations found.\n";
|
|
114
|
+
exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$phpCount = count(array_filter($violations, static fn(array $v): bool => $v['extension'] === 'php'));
|
|
118
|
+
$jsonCount = count(array_filter($violations, static fn(array $v): bool => $v['extension'] === 'json'));
|
|
119
|
+
|
|
120
|
+
echo "Found " . count($violations) . " violation(s)";
|
|
121
|
+
|
|
122
|
+
if ($jsonCount > 0) {
|
|
123
|
+
echo " ($phpCount in PHP files, $jsonCount in JSON files — JSON cannot be auto-fixed)";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
echo ":\n\n";
|
|
127
|
+
|
|
128
|
+
printf("%-4s %-65s %-6s %-55s %s\n", '#', 'Data File', 'Line', 'Violation', 'Owner');
|
|
129
|
+
echo str_repeat('-', 185) . "\n";
|
|
130
|
+
|
|
131
|
+
foreach ($violations as $i => $v) {
|
|
132
|
+
$prefix = $v['extension'] === 'json' ? '[JSON] ' : '';
|
|
133
|
+
printf(
|
|
134
|
+
"%-4d %-65s %-6d %-55s %s\n",
|
|
135
|
+
$i + 1,
|
|
136
|
+
$prefix . $v['file'],
|
|
137
|
+
$v['line'],
|
|
138
|
+
$v['full'],
|
|
139
|
+
$v['owner']
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
echo "\nPHP violations (auto-fixable): $phpCount\n";
|
|
144
|
+
echo "JSON violations (manual fix needed): $jsonCount\n";
|
|
145
|
+
|