@open-agent-toolkit/cli 0.1.64 → 0.1.65
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/assets/docs/cli-utilities/backlog-lifecycle.md +2 -2
- package/assets/docs/cli-utilities/config-and-local-state.md +20 -6
- package/assets/docs/cli-utilities/tool-packs.md +1 -0
- package/assets/docs/cli-utilities/workflow-gates.md +3 -1
- package/assets/docs/contributing/code.md +18 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-pjm-add-backlog-item/SKILL.md +17 -72
- package/assets/skills/oat-project-summary/SKILL.md +9 -4
- package/assets/templates/plan.md +2 -0
- package/dist/commands/backlog/archive.d.ts.map +1 -1
- package/dist/commands/backlog/archive.js +5 -5
- package/dist/commands/backlog/index.d.ts +4 -0
- package/dist/commands/backlog/index.d.ts.map +1 -1
- package/dist/commands/backlog/index.js +55 -0
- package/dist/commands/backlog/new.d.ts +27 -0
- package/dist/commands/backlog/new.d.ts.map +1 -0
- package/dist/commands/backlog/new.js +158 -0
- package/dist/commands/backlog/regenerate-index.d.ts.map +1 -1
- package/dist/commands/backlog/regenerate-index.js +42 -3
- package/dist/commands/decision/index.d.ts.map +1 -1
- package/dist/commands/decision/index.js +4 -0
- package/dist/commands/decision/new.d.ts +2 -0
- package/dist/commands/decision/new.d.ts.map +1 -1
- package/dist/commands/decision/new.js +4 -2
- package/dist/commands/doctor/index.d.ts +1 -0
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +3 -0
- package/dist/commands/doctor/stale-invocations.d.ts +3 -0
- package/dist/commands/doctor/stale-invocations.d.ts.map +1 -0
- package/dist/commands/doctor/stale-invocations.js +173 -0
- package/dist/commands/gate/index.d.ts +1 -0
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +5 -1
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +24 -9
- package/dist/commands/tools/update/index.d.ts.map +1 -1
- package/dist/commands/tools/update/index.js +22 -12
- package/package.json +2 -2
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { access, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { stripTemplateFrontmatter } from '../shared/strip-template-frontmatter.js';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { initializeBacklog } from './init.js';
|
|
6
|
+
import { regenerateBacklogIndex, } from './regenerate-index.js';
|
|
7
|
+
import { generateBacklogId } from './shared/generate-id.js';
|
|
8
|
+
const PRIORITIES = ['urgent', 'high', 'medium', 'low', 'none'];
|
|
9
|
+
const SCOPES = ['idea', 'task', 'feature', 'initiative'];
|
|
10
|
+
const SCOPE_ESTIMATES = ['XS', 'S', 'M', 'L', 'XL', 'XXL'];
|
|
11
|
+
const DESCRIPTION_PLACEHOLDER = '{Describe the problem, request, or capability tracked by this backlog item.}';
|
|
12
|
+
const DEFAULT_DEPENDENCIES = {
|
|
13
|
+
initializeBacklog,
|
|
14
|
+
regenerateBacklogIndex,
|
|
15
|
+
};
|
|
16
|
+
async function pathExists(path) {
|
|
17
|
+
try {
|
|
18
|
+
await access(path);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
23
|
+
? String(error.code)
|
|
24
|
+
: null;
|
|
25
|
+
if (code !== 'ENOENT') {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function resolveBacklogTemplate(assetsRoot, templatesRoot) {
|
|
32
|
+
const candidates = [
|
|
33
|
+
...(templatesRoot ? [join(templatesRoot, 'backlog-item.md')] : []),
|
|
34
|
+
join(assetsRoot, 'templates', 'backlog-item.md'),
|
|
35
|
+
];
|
|
36
|
+
for (const path of candidates) {
|
|
37
|
+
try {
|
|
38
|
+
return { content: await readFile(path, 'utf8'), path };
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
42
|
+
? String(error.code)
|
|
43
|
+
: null;
|
|
44
|
+
if (code !== 'ENOENT') {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw new Error('Template backlog-item.md was not found in repo-local templates or bundled assets.');
|
|
50
|
+
}
|
|
51
|
+
function normalizeChoice(field, value, fallback, allowed, transform) {
|
|
52
|
+
const normalized = transform(value ?? fallback);
|
|
53
|
+
if (!allowed.includes(normalized)) {
|
|
54
|
+
throw new Error(`Invalid backlog ${field} "${value}". Valid values: ${allowed.join(', ')}.`);
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
function normalizeInputs(options) {
|
|
59
|
+
const title = options.title.trim();
|
|
60
|
+
if (!title ||
|
|
61
|
+
title.includes('\r') ||
|
|
62
|
+
title.includes('\n') ||
|
|
63
|
+
title.includes('\0')) {
|
|
64
|
+
throw new Error('Backlog title must be a non-empty single line.');
|
|
65
|
+
}
|
|
66
|
+
const date = new Date(options.createdAt ?? new Date().toISOString());
|
|
67
|
+
if (Number.isNaN(date.getTime())) {
|
|
68
|
+
throw new Error(`Invalid backlog creation timestamp: ${options.createdAt}`);
|
|
69
|
+
}
|
|
70
|
+
const priority = normalizeChoice('priority', options.priority, 'medium', PRIORITIES, (value) => value.trim().toLowerCase());
|
|
71
|
+
const scope = normalizeChoice('scope', options.scope, 'task', SCOPES, (value) => value.trim().toLowerCase());
|
|
72
|
+
const scopeEstimate = options.scopeEstimate === undefined
|
|
73
|
+
? null
|
|
74
|
+
: normalizeChoice('scope estimate', options.scopeEstimate, '', SCOPE_ESTIMATES, (value) => value.trim().toUpperCase());
|
|
75
|
+
const labels = (options.labels ?? []).map((label) => label.trim());
|
|
76
|
+
if (labels.some((label) => !label ||
|
|
77
|
+
label.includes('\r') ||
|
|
78
|
+
label.includes('\n') ||
|
|
79
|
+
label.includes('\0'))) {
|
|
80
|
+
throw new Error('Backlog labels must be non-empty single-line values.');
|
|
81
|
+
}
|
|
82
|
+
if (options.description !== undefined &&
|
|
83
|
+
(!options.description.trim() || options.description.includes('\0'))) {
|
|
84
|
+
throw new Error('Backlog description must contain non-empty text.');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
title,
|
|
88
|
+
timestamp: date.toISOString(),
|
|
89
|
+
priority,
|
|
90
|
+
scope,
|
|
91
|
+
scopeEstimate,
|
|
92
|
+
labels,
|
|
93
|
+
description: options.description,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function renderBacklogItem(template, values) {
|
|
97
|
+
let body = stripTemplateFrontmatter(template).replace(/^\r?\n+/, '');
|
|
98
|
+
if (values.description !== undefined) {
|
|
99
|
+
body = body.replace(DESCRIPTION_PLACEHOLDER, () => values.description);
|
|
100
|
+
}
|
|
101
|
+
const frontmatter = YAML.stringify({
|
|
102
|
+
id: values.id,
|
|
103
|
+
title: values.title,
|
|
104
|
+
status: 'open',
|
|
105
|
+
priority: values.priority,
|
|
106
|
+
scope: values.scope,
|
|
107
|
+
scope_estimate: values.scopeEstimate,
|
|
108
|
+
labels: values.labels,
|
|
109
|
+
assignee: null,
|
|
110
|
+
created: values.timestamp,
|
|
111
|
+
updated: values.timestamp,
|
|
112
|
+
associated_issues: [],
|
|
113
|
+
external_plans: [],
|
|
114
|
+
}).trimEnd();
|
|
115
|
+
return `---\n${frontmatter}\n---\n\n${body.trimEnd()}\n`;
|
|
116
|
+
}
|
|
117
|
+
export async function createBacklogItem(options, overrides = {}) {
|
|
118
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
|
|
119
|
+
const values = normalizeInputs(options);
|
|
120
|
+
const id = generateBacklogId(values.title, values.timestamp);
|
|
121
|
+
const filePath = join(options.backlogRoot, 'items', `${id}.md`);
|
|
122
|
+
const archivedPath = join(options.backlogRoot, 'archived', `${id}.md`);
|
|
123
|
+
if ((await pathExists(filePath)) || (await pathExists(archivedPath))) {
|
|
124
|
+
throw new Error(`Backlog item ${id} already exists. Use a more specific title to disambiguate.`);
|
|
125
|
+
}
|
|
126
|
+
const template = await resolveBacklogTemplate(options.assetsRoot, options.templatesRoot);
|
|
127
|
+
const content = renderBacklogItem(template.content, { id, ...values });
|
|
128
|
+
await dependencies.initializeBacklog(options.backlogRoot);
|
|
129
|
+
const indexPath = join(options.backlogRoot, 'index.md');
|
|
130
|
+
const originalIndex = await readFile(indexPath, 'utf8');
|
|
131
|
+
await writeFile(filePath, content, { encoding: 'utf8', flag: 'wx' });
|
|
132
|
+
let index;
|
|
133
|
+
try {
|
|
134
|
+
index = await dependencies.regenerateBacklogIndex(options.backlogRoot);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const rollback = await Promise.allSettled([
|
|
138
|
+
rm(filePath),
|
|
139
|
+
writeFile(indexPath, originalIndex, 'utf8'),
|
|
140
|
+
]);
|
|
141
|
+
const rollbackErrors = rollback
|
|
142
|
+
.filter((result) => result.status === 'rejected')
|
|
143
|
+
.map((result) => result.reason);
|
|
144
|
+
if (rollbackErrors.length > 0) {
|
|
145
|
+
const rollbackError = new AggregateError([error, ...rollbackErrors], `Backlog item ${id} creation failed and rollback was incomplete.`);
|
|
146
|
+
rollbackError.cause = error;
|
|
147
|
+
throw rollbackError;
|
|
148
|
+
}
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
backlogRoot: options.backlogRoot,
|
|
154
|
+
filePath,
|
|
155
|
+
templatePath: template.path,
|
|
156
|
+
index,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regenerate-index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/regenerate-index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"regenerate-index.d.ts","sourceRoot":"","sources":["../../../src/commands/backlog/regenerate-index.ts"],"names":[],"mappings":"AAgJA,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAsB,sBAAsB,CAC1C,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,4BAA4B,CAAC,CA6DvC"}
|
|
@@ -52,9 +52,29 @@ function compareItems(a, b) {
|
|
|
52
52
|
}
|
|
53
53
|
return 0;
|
|
54
54
|
}
|
|
55
|
+
function encodeMarkdownTableCell(value) {
|
|
56
|
+
return value
|
|
57
|
+
.replaceAll('&', '&')
|
|
58
|
+
.replaceAll('\\', '\\\\')
|
|
59
|
+
.replaceAll('|', '\\|')
|
|
60
|
+
.replaceAll('<', '<')
|
|
61
|
+
.replaceAll('>', '>')
|
|
62
|
+
.replaceAll('\r', ' ')
|
|
63
|
+
.replaceAll('\n', ' ');
|
|
64
|
+
}
|
|
55
65
|
function renderManagedSection(items) {
|
|
56
66
|
const rows = items.length > 0
|
|
57
|
-
? items.map((item) =>
|
|
67
|
+
? items.map((item) => {
|
|
68
|
+
const cells = [
|
|
69
|
+
item.id,
|
|
70
|
+
item.title,
|
|
71
|
+
item.status,
|
|
72
|
+
item.priority,
|
|
73
|
+
item.scope,
|
|
74
|
+
item.scopeEstimate,
|
|
75
|
+
].map(encodeMarkdownTableCell);
|
|
76
|
+
return `| ${cells.join(' | ')} |`;
|
|
77
|
+
})
|
|
58
78
|
: ['| _No backlog items yet_ | - | - | - | - | - |'];
|
|
59
79
|
return [
|
|
60
80
|
INDEX_START,
|
|
@@ -64,6 +84,23 @@ function renderManagedSection(items) {
|
|
|
64
84
|
INDEX_END,
|
|
65
85
|
].join('\n');
|
|
66
86
|
}
|
|
87
|
+
function findStandaloneMarker(content, marker, fromIndex = 0) {
|
|
88
|
+
let lineStart = 0;
|
|
89
|
+
while (lineStart <= content.length) {
|
|
90
|
+
const lineEnd = content.indexOf('\n', lineStart);
|
|
91
|
+
const contentEnd = lineEnd === -1 ? content.length : lineEnd;
|
|
92
|
+
const rawLine = content.slice(lineStart, contentEnd);
|
|
93
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
94
|
+
if (lineStart >= fromIndex && line === marker) {
|
|
95
|
+
return lineStart;
|
|
96
|
+
}
|
|
97
|
+
if (lineEnd === -1) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
lineStart = lineEnd + 1;
|
|
101
|
+
}
|
|
102
|
+
return -1;
|
|
103
|
+
}
|
|
67
104
|
export async function regenerateBacklogIndex(backlogRoot) {
|
|
68
105
|
const itemsDir = join(backlogRoot, 'items');
|
|
69
106
|
const indexPath = join(backlogRoot, 'index.md');
|
|
@@ -85,8 +122,10 @@ export async function regenerateBacklogIndex(backlogRoot) {
|
|
|
85
122
|
}
|
|
86
123
|
items.sort(compareItems);
|
|
87
124
|
const content = await readFile(indexPath, 'utf8');
|
|
88
|
-
const startIndex = content
|
|
89
|
-
const endIndex =
|
|
125
|
+
const startIndex = findStandaloneMarker(content, INDEX_START);
|
|
126
|
+
const endIndex = startIndex === -1
|
|
127
|
+
? -1
|
|
128
|
+
: findStandaloneMarker(content, INDEX_END, startIndex + INDEX_START.length);
|
|
90
129
|
if (startIndex === -1 || endIndex === -1) {
|
|
91
130
|
throw new Error(`Managed backlog index markers missing in ${indexPath}. Expected the exact marker pair:\n${INDEX_START}\n${INDEX_END}\nRun \`oat backlog init\` if the backlog scaffold is missing, or restore those exact markers in \`backlog/index.md\` before rerunning \`oat backlog regenerate-index\`.`);
|
|
92
131
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/decision/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,yBAAyB,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/decision/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAuB,MAAM,sBAAsB,CAAC;AAEhF,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,yBAAyB,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAyB7D,UAAU,2BAA2B;IACnC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,kBAAkB,EAAE,OAAO,kBAAkB,CAAC;IAC9C,iBAAiB,EAAE,OAAO,iBAAiB,CAAC;IAC5C,yBAAyB,EAAE,OAAO,yBAAyB,CAAC;IAC5D,uBAAuB,EAAE,OAAO,uBAAuB,CAAC;IACxD,oBAAoB,EAAE,OAAO,oBAAoB,CAAC;IAClD,sBAAsB,EAAE,OAAO,sBAAsB,CAAC;CACvD;AA8CD,wBAAgB,qBAAqB,CACnC,SAAS,GAAE,OAAO,CAAC,2BAA2B,CAAM,GACnD,OAAO,CAmMT"}
|
|
@@ -102,6 +102,8 @@ export function createDecisionCommand(overrides = {}) {
|
|
|
102
102
|
.option('--decisions-root <path>', 'Decisions root directory (defaults to .oat/repo/reference/decisions)')
|
|
103
103
|
.option('--status <status>', 'Decision status', 'proposed')
|
|
104
104
|
.option('--context <text>', 'Initial context body text')
|
|
105
|
+
.option('--decision <text>', 'Initial decision body text')
|
|
106
|
+
.option('--consequences <text>', 'Initial consequences body text')
|
|
105
107
|
.option('--created-at <timestamp>', 'Creation timestamp seed for reproducible ID generation')
|
|
106
108
|
.action(async (title, options, command) => {
|
|
107
109
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
@@ -116,6 +118,8 @@ export function createDecisionCommand(overrides = {}) {
|
|
|
116
118
|
title,
|
|
117
119
|
status: options.status,
|
|
118
120
|
context: options.context,
|
|
121
|
+
decision: options.decision,
|
|
122
|
+
consequences: options.consequences,
|
|
119
123
|
createdAt: options.createdAt,
|
|
120
124
|
});
|
|
121
125
|
if (context.json) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../../src/commands/decision/new.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,2BAA2B;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"new.d.ts","sourceRoot":"","sources":["../../../src/commands/decision/new.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,2BAA2B;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA4HD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,0BAA0B,CAAC,CAgDrC"}
|
|
@@ -68,8 +68,8 @@ function renderDecisionRecord(template, options) {
|
|
|
68
68
|
date: options.date,
|
|
69
69
|
status: options.status,
|
|
70
70
|
context: options.context,
|
|
71
|
-
decision:
|
|
72
|
-
consequences:
|
|
71
|
+
decision: options.decision,
|
|
72
|
+
consequences: options.consequences,
|
|
73
73
|
});
|
|
74
74
|
const frontmatter = YAML.stringify({
|
|
75
75
|
id: options.id,
|
|
@@ -95,6 +95,8 @@ export async function createDecisionRecord(options) {
|
|
|
95
95
|
date: createdAt.date,
|
|
96
96
|
status,
|
|
97
97
|
context: options.context ?? 'TODO',
|
|
98
|
+
decision: options.decision ?? 'TODO',
|
|
99
|
+
consequences: options.consequences ?? 'TODO',
|
|
98
100
|
});
|
|
99
101
|
await assertDecisionIndexScaffold(options.decisionsRoot);
|
|
100
102
|
await mkdir(dirname(filePath), { recursive: true });
|
|
@@ -29,6 +29,7 @@ interface DoctorDependencies {
|
|
|
29
29
|
processEnv: NodeJS.ProcessEnv;
|
|
30
30
|
runPjmDoctorChecks: (repoRoot: string, options?: PjmDoctorOptions) => Promise<DoctorCheck[]>;
|
|
31
31
|
checkSkillVersions: (scopeRoot: string, assetsRoot: string, pathExists: (path: string) => Promise<boolean>) => Promise<SkillVersionReport>;
|
|
32
|
+
checkStaleInvocations: (repoRoot: string) => Promise<DoctorCheck>;
|
|
32
33
|
}
|
|
33
34
|
interface OutdatedSkillVersion {
|
|
34
35
|
skill: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAY9B,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,cAAc,EAGnB,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAO9D,OAAO,EAGL,KAAK,8BAA8B,EACnC,KAAK,yBAAyB,EAC/B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,mCAAmC,EACnC,0BAA0B,EAC3B,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,KAAK,WAAW,EAAuB,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ import { validateMatrixCell, } from '../../providers/identity/availability.js';
|
|
|
23
23
|
import { createDispatchValidationPassContext, validateDispatchMatrixRefs, } from '../../providers/identity/dispatch-validation.js';
|
|
24
24
|
import { formatDoctorResults } from '../../ui/output.js';
|
|
25
25
|
import { Command } from 'commander';
|
|
26
|
+
import { checkStaleInvocations } from './stale-invocations.js';
|
|
26
27
|
async function pathExistsDefault(path) {
|
|
27
28
|
try {
|
|
28
29
|
await access(path);
|
|
@@ -143,6 +144,7 @@ function createDependencies() {
|
|
|
143
144
|
// Default binding remains self-contained, but still honors the caller-
|
|
144
145
|
// provided pathExists dependency from runChecksForScope when available.
|
|
145
146
|
checkSkillVersions: (scopeRoot, assetsRoot, pathExists = pathExistsDefault) => checkSkillVersionsDefault(scopeRoot, assetsRoot, pathExists),
|
|
147
|
+
checkStaleInvocations,
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
150
|
function collectDispatchMatrixCellRefs(layers) {
|
|
@@ -505,6 +507,7 @@ async function runChecksForScope(scope, scopeRoot, userConfigDir, dependencies)
|
|
|
505
507
|
});
|
|
506
508
|
checks.push(...(await runCodexChecksForScope(scope, scopeRoot, dirname(userConfigDir), dependencies)));
|
|
507
509
|
if (scope === 'project') {
|
|
510
|
+
checks.push(await dependencies.checkStaleInvocations(scopeRoot));
|
|
508
511
|
try {
|
|
509
512
|
const assetsRoot = await dependencies.resolveAssetsRoot();
|
|
510
513
|
const skillVersions = await dependencies.checkSkillVersions(scopeRoot, assetsRoot, dependencies.pathExists);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stale-invocations.d.ts","sourceRoot":"","sources":["../../../src/commands/doctor/stale-invocations.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAoK9C,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,WAAW,CAAC,CA6CtB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
const KNOWN_STALE_INVOCATIONS = [
|
|
4
|
+
{
|
|
5
|
+
id: 'global-scope-sync',
|
|
6
|
+
pattern: /\boat\s+--scope(?:\s+|=)all\s+sync\b/g,
|
|
7
|
+
replacement: 'oat sync --scope all',
|
|
8
|
+
},
|
|
9
|
+
];
|
|
10
|
+
const SCANNED_EXTENSIONS = new Set([
|
|
11
|
+
'.bash',
|
|
12
|
+
'.cjs',
|
|
13
|
+
'.fish',
|
|
14
|
+
'.js',
|
|
15
|
+
'.json',
|
|
16
|
+
'.jsonc',
|
|
17
|
+
'.md',
|
|
18
|
+
'.mdc',
|
|
19
|
+
'.mdx',
|
|
20
|
+
'.mjs',
|
|
21
|
+
'.mts',
|
|
22
|
+
'.sh',
|
|
23
|
+
'.toml',
|
|
24
|
+
'.ts',
|
|
25
|
+
'.yaml',
|
|
26
|
+
'.yml',
|
|
27
|
+
'.zsh',
|
|
28
|
+
]);
|
|
29
|
+
const EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
30
|
+
'.git',
|
|
31
|
+
'.next',
|
|
32
|
+
'.turbo',
|
|
33
|
+
'archived',
|
|
34
|
+
'build',
|
|
35
|
+
'coverage',
|
|
36
|
+
'dist',
|
|
37
|
+
'node_modules',
|
|
38
|
+
'out',
|
|
39
|
+
]);
|
|
40
|
+
const GENERATED_PROVIDER_ROOTS = new Set([
|
|
41
|
+
'.claude',
|
|
42
|
+
'.codex',
|
|
43
|
+
'.cursor',
|
|
44
|
+
'.gemini',
|
|
45
|
+
]);
|
|
46
|
+
const GENERATED_GITHUB_DIRECTORIES = new Set([
|
|
47
|
+
'agents',
|
|
48
|
+
'instructions',
|
|
49
|
+
'prompts',
|
|
50
|
+
'skills',
|
|
51
|
+
]);
|
|
52
|
+
const ALLOW_MARKER = 'oat-doctor: allow-stale-invocation';
|
|
53
|
+
const MAX_FILE_BYTES = 1024 * 1024;
|
|
54
|
+
const MAX_SCANNED_FILES = 10_000;
|
|
55
|
+
const MAX_REPORTED_MATCHES = 50;
|
|
56
|
+
function isExcludedDirectory(parts) {
|
|
57
|
+
const name = parts.at(-1);
|
|
58
|
+
if (!name || EXCLUDED_DIRECTORY_NAMES.has(name)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (parts.length === 1 && name === '.worktrees') {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (GENERATED_PROVIDER_ROOTS.has(name)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const parent = parts.at(-2);
|
|
68
|
+
if (parent === '.oat' && (name === 'projects' || name === 'sync')) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return parent === '.github' && GENERATED_GITHUB_DIRECTORIES.has(name);
|
|
72
|
+
}
|
|
73
|
+
function isScannableFile(relativePath) {
|
|
74
|
+
const parts = relativePath.split('/');
|
|
75
|
+
const fileName = parts.at(-1) ?? '';
|
|
76
|
+
if (/\.(?:test|spec)\.[^.]+$/.test(fileName)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (fileName === 'copilot-instructions.md' && parts.at(-2) === '.github') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return SCANNED_EXTENSIONS.has(extname(fileName).toLowerCase());
|
|
83
|
+
}
|
|
84
|
+
async function listScannableFiles(repoRoot) {
|
|
85
|
+
const files = [];
|
|
86
|
+
async function walk(parts) {
|
|
87
|
+
if (files.length >= MAX_SCANNED_FILES) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const entries = await readdir(join(repoRoot, ...parts), {
|
|
91
|
+
withFileTypes: true,
|
|
92
|
+
});
|
|
93
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (files.length >= MAX_SCANNED_FILES) {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (entry.isSymbolicLink()) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const childParts = [...parts, entry.name];
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (!isExcludedDirectory(childParts)) {
|
|
104
|
+
await walk(childParts);
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const relativePath = childParts.join('/');
|
|
109
|
+
if (entry.isFile() && isScannableFile(relativePath)) {
|
|
110
|
+
files.push(relativePath);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
await walk([]);
|
|
115
|
+
return files;
|
|
116
|
+
}
|
|
117
|
+
function findMatches(relativePath, content) {
|
|
118
|
+
const matches = [];
|
|
119
|
+
const lines = content.split(/\r?\n/);
|
|
120
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
121
|
+
if (line.includes(ALLOW_MARKER)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const stale of KNOWN_STALE_INVOCATIONS) {
|
|
125
|
+
stale.pattern.lastIndex = 0;
|
|
126
|
+
for (const match of line.matchAll(stale.pattern)) {
|
|
127
|
+
matches.push({
|
|
128
|
+
relativePath,
|
|
129
|
+
line: lineIndex + 1,
|
|
130
|
+
invocation: match[0],
|
|
131
|
+
replacement: stale.replacement,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return matches;
|
|
137
|
+
}
|
|
138
|
+
export async function checkStaleInvocations(repoRoot) {
|
|
139
|
+
const matches = [];
|
|
140
|
+
for (const relativePath of await listScannableFiles(repoRoot)) {
|
|
141
|
+
const absolutePath = join(repoRoot, relativePath);
|
|
142
|
+
const metadata = await stat(absolutePath);
|
|
143
|
+
if (metadata.size > MAX_FILE_BYTES) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
matches.push(...findMatches(relativePath, await readFile(absolutePath, 'utf8')));
|
|
147
|
+
if (matches.length >= MAX_REPORTED_MATCHES) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (matches.length === 0) {
|
|
152
|
+
return {
|
|
153
|
+
name: 'project:stale_invocations',
|
|
154
|
+
description: 'Known-stale CLI invocation grammar',
|
|
155
|
+
status: 'pass',
|
|
156
|
+
message: 'No known-stale CLI invocations found in repository scripts or documentation.',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const evidence = matches
|
|
160
|
+
.slice(0, MAX_REPORTED_MATCHES)
|
|
161
|
+
.map((match) => `${match.relativePath}:${match.line} (\`${match.invocation}\` → \`${match.replacement}\`)`)
|
|
162
|
+
.join(', ');
|
|
163
|
+
const replacements = [...new Set(matches.map((match) => match.replacement))];
|
|
164
|
+
return {
|
|
165
|
+
name: 'project:stale_invocations',
|
|
166
|
+
description: 'Known-stale CLI invocation grammar',
|
|
167
|
+
status: 'warn',
|
|
168
|
+
message: `Known-stale CLI invocations found: ${evidence}.`,
|
|
169
|
+
fix: `Update each invocation to the current command grammar: ${replacements
|
|
170
|
+
.map((replacement) => `\`${replacement}\``)
|
|
171
|
+
.join(', ')}.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -26,6 +26,7 @@ interface ProcessRunOptions {
|
|
|
26
26
|
livenessIntervalMs?: number;
|
|
27
27
|
onLiveness?: (snapshot: GateLivenessSnapshot) => void;
|
|
28
28
|
purpose: 'host-detection' | 'availability' | 'execute';
|
|
29
|
+
stdin: 'ignore' | 'inherit';
|
|
29
30
|
stdio: 'ignore' | 'inherit' | 'pipe';
|
|
30
31
|
timeoutMs: number;
|
|
31
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -206,7 +206,9 @@ async function runChildProcess(command, args, options) {
|
|
|
206
206
|
const child = spawn(command, args, {
|
|
207
207
|
cwd: options.cwd,
|
|
208
208
|
env: options.env,
|
|
209
|
-
stdio: options.stdio === 'pipe'
|
|
209
|
+
stdio: options.stdio === 'pipe'
|
|
210
|
+
? [options.stdin, 'pipe', 'pipe']
|
|
211
|
+
: options.stdio,
|
|
210
212
|
});
|
|
211
213
|
const recordActivity = () => {
|
|
212
214
|
lastActivityAt = Date.now();
|
|
@@ -814,6 +816,7 @@ async function checkArgv(argv, purpose, context, dependencies) {
|
|
|
814
816
|
cwd: context.cwd,
|
|
815
817
|
env: dependencies.processEnv,
|
|
816
818
|
purpose,
|
|
819
|
+
stdin: 'ignore',
|
|
817
820
|
stdio: 'ignore',
|
|
818
821
|
timeoutMs: GATE_CHECK_TIMEOUT_MS,
|
|
819
822
|
});
|
|
@@ -898,6 +901,7 @@ async function executeTarget(selected, prompt, context, dependencies) {
|
|
|
898
901
|
}
|
|
899
902
|
},
|
|
900
903
|
purpose: 'execute',
|
|
904
|
+
stdin: 'ignore',
|
|
901
905
|
stdio: 'pipe',
|
|
902
906
|
timeoutMs,
|
|
903
907
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/new/scaffold.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wBAAwB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B,WAAW,GACX,kBAAkB,GAClB,qBAAqB,GACrB,iBAAiB,GACjB,QAAQ,CAAC;AAEb,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,oBAAoB,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../../../src/commands/project/new/scaffold.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wBAAwB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B,WAAW,GACX,kBAAkB,GAClB,qBAAqB,GACrB,iBAAiB,GACjB,QAAQ,CAAC;AAEb,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,kBAAkB,EAAE,OAAO,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,oBAAoB,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA4UD,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAoFhC"}
|
|
@@ -20,6 +20,7 @@ const TEMPLATES_BY_MODE = {
|
|
|
20
20
|
quick: ['state.md', 'discovery.md', 'plan.md', 'implementation.md'],
|
|
21
21
|
import: ['state.md', 'plan.md', 'implementation.md'],
|
|
22
22
|
};
|
|
23
|
+
const OAT_PLACEHOLDER_PATTERN = /(?<!\{)\{\s*(OAT_[A-Z0-9_]+)\s*\}(?!\})/g;
|
|
23
24
|
const STATE_TEMPLATE_BY_MODE = {
|
|
24
25
|
'spec-driven': {
|
|
25
26
|
hillCheckpoints: "['discovery', 'design']",
|
|
@@ -79,6 +80,17 @@ const STATE_TEMPLATE_BY_MODE = {
|
|
|
79
80
|
nextMilestone: 'Run `oat-project-import-plan` to normalize the external plan',
|
|
80
81
|
},
|
|
81
82
|
};
|
|
83
|
+
function replaceOatPlaceholders(template, replacements) {
|
|
84
|
+
return template.replace(OAT_PLACEHOLDER_PATTERN, (placeholder, token) => replacements[token] ?? placeholder);
|
|
85
|
+
}
|
|
86
|
+
function assertNoUnresolvedOatPlaceholders(rendered, templateFile) {
|
|
87
|
+
const unresolved = [
|
|
88
|
+
...new Set([...rendered.matchAll(OAT_PLACEHOLDER_PATTERN)].map(([placeholder]) => placeholder)),
|
|
89
|
+
];
|
|
90
|
+
if (unresolved.length > 0) {
|
|
91
|
+
throw new Error(`Cannot scaffold ${templateFile}: unresolved OAT placeholder(s): ${unresolved.join(', ')}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
82
94
|
function validateProjectName(name) {
|
|
83
95
|
if (name.startsWith('-')) {
|
|
84
96
|
throw new Error(`Invalid project name "${name}". Project names must not start with a dash.`);
|
|
@@ -89,17 +101,19 @@ function validateProjectName(name) {
|
|
|
89
101
|
}
|
|
90
102
|
function applyTemplateReplacements(template, projectName, today, nowUtc, mode) {
|
|
91
103
|
const stateContent = STATE_TEMPLATE_BY_MODE[mode];
|
|
92
|
-
|
|
104
|
+
const oatReplacements = {
|
|
105
|
+
OAT_HILL_CHECKPOINTS: stateContent.hillCheckpoints,
|
|
106
|
+
OAT_WORKFLOW_MODE: mode,
|
|
107
|
+
OAT_PHASE: stateContent.phase,
|
|
108
|
+
OAT_STATUS: stateContent.status,
|
|
109
|
+
OAT_CURRENT_PHASE: stateContent.currentPhase,
|
|
110
|
+
OAT_ARTIFACTS: stateContent.artifacts.join('\n'),
|
|
111
|
+
OAT_PROGRESS: stateContent.progress.join('\n'),
|
|
112
|
+
OAT_NEXT_MILESTONE: stateContent.nextMilestone,
|
|
113
|
+
};
|
|
114
|
+
return replaceOatPlaceholders(template, oatReplacements)
|
|
93
115
|
.replaceAll('{Project Name}', projectName)
|
|
94
116
|
.replaceAll('YYYY-MM-DD', today)
|
|
95
|
-
.replaceAll('{OAT_HILL_CHECKPOINTS}', stateContent.hillCheckpoints)
|
|
96
|
-
.replaceAll('{OAT_WORKFLOW_MODE}', mode)
|
|
97
|
-
.replaceAll('{OAT_PHASE}', stateContent.phase)
|
|
98
|
-
.replaceAll('{OAT_STATUS}', stateContent.status)
|
|
99
|
-
.replaceAll('{OAT_CURRENT_PHASE}', stateContent.currentPhase)
|
|
100
|
-
.replaceAll('{OAT_ARTIFACTS}', stateContent.artifacts.join('\n'))
|
|
101
|
-
.replaceAll('{OAT_PROGRESS}', stateContent.progress.join('\n'))
|
|
102
|
-
.replaceAll('{OAT_NEXT_MILESTONE}', stateContent.nextMilestone)
|
|
103
117
|
.replaceAll(/oat_project_created:\s*null/gi, `oat_project_created: "${nowUtc}"`)
|
|
104
118
|
.replaceAll(/oat_project_state_updated:\s*null/gi, `oat_project_state_updated: "${nowUtc}"`)
|
|
105
119
|
.replaceAll(/\n?oat_template:\s*true\s*\n/gi, '\n')
|
|
@@ -176,6 +190,7 @@ async function scaffoldModeTemplates(userOatRoot, repoRoot, projectPath, project
|
|
|
176
190
|
}
|
|
177
191
|
const template = await readFile(src, 'utf8');
|
|
178
192
|
const rendered = applyTemplateReplacements(template, projectName, today, nowUtc, mode);
|
|
193
|
+
assertNoUnresolvedOatPlaceholders(rendered, templateFile);
|
|
179
194
|
if (templateFile === 'state.md') {
|
|
180
195
|
assertValidProjectStateContent(rendered, { filePath: dest });
|
|
181
196
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/tools/update/index.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAKvE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAE7B,MAAM,gBAAgB,CAAC;AAgBxB,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAClD,MAAM,EAAE,CAaV;AAgCD,wBAAgB,wBAAwB,CACtC,YAAY,GAAE,uBAA6C,EAC3D,gBAAgB,GAAE,oBAA8C,GAC/D,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/commands/tools/update/index.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,oBAAoB,EAE1B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAKvE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAE7B,MAAM,gBAAgB,CAAC;AAgBxB,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAA;CAAE,GAClD,MAAM,EAAE,CAaV;AAgCD,wBAAgB,wBAAwB,CACtC,YAAY,GAAE,uBAA6C,EAC3D,gBAAgB,GAAE,oBAA8C,GAC/D,OAAO,CAqKT;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAI7E;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,YAAY,GACnB,OAAO,CAOT;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,QAAQ,EACd,MAAM,EAAE,OAAO,GACd,MAAM,CAMR"}
|