ace-pack 0.1.0

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.
@@ -0,0 +1,206 @@
1
+ import { execFile } from 'node:child_process'
2
+ import path from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+ import { promisify } from 'node:util'
5
+
6
+ import {
7
+ extractLabeledValue,
8
+ extractMarkdownSection,
9
+ extractUnresolvedReflections,
10
+ formatTimestamp,
11
+ getArgValue,
12
+ parseCliArgs,
13
+ readTextIfExists,
14
+ writeAceBanner,
15
+ writeTextFile,
16
+ } from './ai-memory-utils.mjs'
17
+ import { classifyRepositoryTask } from './ai-task-classify.mjs'
18
+
19
+ const execFileAsync = promisify(execFile)
20
+ const PLACEHOLDER_PATTERN = /\[[^\]]+\]|\bTODO\b|\bTBD\b|No .* recorded/i
21
+
22
+ export function validateFinishRequirements({
23
+ classification,
24
+ currentTaskContent,
25
+ handoffContent,
26
+ reflectionLogContent,
27
+ }) {
28
+ const missing = []
29
+ const businessValue = extractMarkdownSection(
30
+ currentTaskContent,
31
+ 'Business Value / Product Alignment',
32
+ )
33
+ const technicalApproach = extractMarkdownSection(currentTaskContent, 'Technical Approach')
34
+ const qualityReview = extractMarkdownSection(handoffContent, 'Quality Review')
35
+
36
+ if (!hasMeaningfulContent(businessValue)) {
37
+ missing.push('Fill .ai/current-task.md Business Value / Product Alignment.')
38
+ }
39
+
40
+ if (classification.designReviewRequired && !hasDesignReview(technicalApproach)) {
41
+ missing.push(
42
+ 'Fill .ai/current-task.md Technical Approach with Option 1, Option 2, and Chosen Approach.',
43
+ )
44
+ }
45
+
46
+ if (
47
+ (classification.tier === 'standard' || classification.tier === 'large') &&
48
+ !hasQualityReview(qualityReview)
49
+ ) {
50
+ missing.push(
51
+ 'Fill .ai/session-handoff.md Quality Review for product, architecture, security, and code quality.',
52
+ )
53
+ }
54
+
55
+ if (classification.tier === 'large' && !hasMeaningfulReflection(reflectionLogContent)) {
56
+ missing.push('Add a compact .ai/reflection-log.md entry for this large task.')
57
+ }
58
+
59
+ return missing
60
+ }
61
+
62
+ export async function archiveCurrentTask(rootDir) {
63
+ const currentTaskPath = path.join(rootDir, '.ai', 'current-task.md')
64
+ const currentTaskContent = await readTextIfExists(currentTaskPath)
65
+
66
+ if (currentTaskContent === null) {
67
+ throw new Error('Cannot archive missing .ai/current-task.md.')
68
+ }
69
+
70
+ const lifecycle = extractMarkdownSection(currentTaskContent, 'Lifecycle')
71
+ const version = extractLabeledValue(lifecycle, 'Version') || 'task'
72
+ const featureName = extractMarkdownSection(currentTaskContent, 'Feature Name') || 'task'
73
+ const timestamp = formatTimestamp(new Date()).replace(/[: ]/g, '-')
74
+ const baseName = `${slugify(version)}-${slugify(featureName)}-${timestamp}`
75
+ let outputPath = path.join(rootDir, '.ai', 'archive', 'tasks', `${baseName}.md`)
76
+ let suffix = 2
77
+
78
+ while ((await readTextIfExists(outputPath)) !== null) {
79
+ outputPath = path.join(rootDir, '.ai', 'archive', 'tasks', `${baseName}-${suffix}.md`)
80
+ suffix += 1
81
+ }
82
+
83
+ await writeTextFile(outputPath, currentTaskContent)
84
+ return outputPath
85
+ }
86
+
87
+ async function main() {
88
+ writeAceBanner()
89
+
90
+ const rawArgs = process.argv.slice(2)
91
+ const args = parseCliArgs(rawArgs)
92
+ const rootDir = path.resolve(process.cwd(), getArgValue(args, 'root') ?? '.')
93
+ const classification = await classifyRepositoryTask(rootDir, {
94
+ overrideReason: getArgValue(args, 'reason'),
95
+ overrideTier: getArgValue(args, 'tier'),
96
+ })
97
+ const [currentTaskContent, handoffContent, reflectionLogContent] = await Promise.all([
98
+ requireAiFile(rootDir, 'current-task.md'),
99
+ requireAiFile(rootDir, 'session-handoff.md'),
100
+ requireAiFile(rootDir, 'reflection-log.md'),
101
+ ])
102
+ const missing = validateFinishRequirements({
103
+ classification,
104
+ currentTaskContent,
105
+ handoffContent,
106
+ reflectionLogContent,
107
+ })
108
+
109
+ if (missing.length > 0) {
110
+ process.stderr.write('Adaptive task finish blocked by missing closeout notes:\n')
111
+
112
+ for (const item of missing) {
113
+ process.stderr.write(`- ${item}\n`)
114
+ }
115
+
116
+ process.exit(1)
117
+ }
118
+
119
+ await runNodeScript(rootDir, 'ai-report-brief.mjs')
120
+
121
+ if (classification.tier === 'large') {
122
+ process.stderr.write(
123
+ 'Large task reminder: update .ai/tech-docs.md for architecture changes and .ai/product-roadmap.md for business or roadmap changes when applicable.\n',
124
+ )
125
+ const archivePath = await archiveCurrentTask(rootDir)
126
+ process.stderr.write(`Archived current task to ${archivePath}\n`)
127
+ await runNodeScript(rootDir, 'ai-report.mjs')
128
+ }
129
+
130
+ process.stderr.write(`Adaptive task finish passed for ${classification.tier} task.\n`)
131
+ }
132
+
133
+ async function requireAiFile(rootDir, fileName) {
134
+ const filePath = path.join(rootDir, '.ai', fileName)
135
+ const content = await readTextIfExists(filePath)
136
+
137
+ if (content === null) {
138
+ throw new Error(`Missing required file: ${filePath}`)
139
+ }
140
+
141
+ return content
142
+ }
143
+
144
+ async function runNodeScript(rootDir, scriptName) {
145
+ await execFileAsync(process.execPath, [path.join(rootDir, 'scripts', scriptName), rootDir], {
146
+ cwd: rootDir,
147
+ env: process.env,
148
+ })
149
+ }
150
+
151
+ function hasDesignReview(section) {
152
+ return (
153
+ hasMeaningfulContent(section) &&
154
+ /Option\s*1:/i.test(section) &&
155
+ /Option\s*2:/i.test(section) &&
156
+ /Chosen Approach:/i.test(section)
157
+ )
158
+ }
159
+
160
+ function hasQualityReview(section) {
161
+ return (
162
+ hasMeaningfulContent(section) &&
163
+ hasQualityLabel(section, 'Product Alignment') &&
164
+ hasQualityLabel(section, 'Architecture') &&
165
+ hasQualityLabel(section, 'Security') &&
166
+ hasQualityLabel(section, 'Code Quality')
167
+ )
168
+ }
169
+
170
+ function hasQualityLabel(section, label) {
171
+ const pattern = new RegExp(
172
+ `${escapeRegExp(label)}:\\s*([\\s\\S]*?)(?=\\n[A-Z][A-Za-z ]+:|$)`,
173
+ 'i',
174
+ )
175
+ const match = section.match(pattern)
176
+
177
+ return Boolean(match?.[1] && hasMeaningfulContent(match[1]))
178
+ }
179
+
180
+ function hasMeaningfulReflection(content) {
181
+ return (
182
+ extractUnresolvedReflections(content, 1).length > 0 || /## Resolved[\s\S]*^### /m.test(content)
183
+ )
184
+ }
185
+
186
+ function hasMeaningfulContent(content) {
187
+ const normalizedContent = content.replace(/\s+/g, ' ').trim()
188
+
189
+ return normalizedContent.length > 0 && !PLACEHOLDER_PATTERN.test(normalizedContent)
190
+ }
191
+
192
+ function slugify(value) {
193
+ return value
194
+ .toLowerCase()
195
+ .replace(/[^a-z0-9._-]+/g, '-')
196
+ .replace(/^-+|-+$/g, '')
197
+ .slice(0, 80)
198
+ }
199
+
200
+ function escapeRegExp(value) {
201
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
202
+ }
203
+
204
+ if (import.meta.url === pathToFileURL(process.argv[1]).href) {
205
+ await main()
206
+ }
@@ -0,0 +1,236 @@
1
+ import path from 'node:path'
2
+
3
+ import {
4
+ formatBullets,
5
+ formatChecklist,
6
+ getArgList,
7
+ getArgValue,
8
+ nowTimestamp,
9
+ parseCliArgs,
10
+ readTextIfExists,
11
+ replaceLabeledValue,
12
+ replaceMarkdownSection,
13
+ writeTextFile,
14
+ } from './ai-memory-utils.mjs'
15
+
16
+ const command = process.argv[2]
17
+ const args = parseCliArgs(process.argv.slice(3))
18
+ const rootDir = process.cwd()
19
+ const aiDir = path.join(rootDir, '.ai')
20
+
21
+ if (!command) {
22
+ throw new Error(
23
+ 'Usage: node ./scripts/ai-update.mjs <task|handoff|log|decision|changed> [--flags]',
24
+ )
25
+ }
26
+
27
+ switch (command) {
28
+ case 'task':
29
+ await updateTask()
30
+ break
31
+ case 'handoff':
32
+ await updateHandoff()
33
+ break
34
+ case 'log':
35
+ await appendWorkLog()
36
+ break
37
+ case 'decision':
38
+ await appendDecision()
39
+ break
40
+ case 'changed':
41
+ await appendChangedFile()
42
+ break
43
+ default:
44
+ throw new Error(`Unknown ai:update command: ${command}`)
45
+ }
46
+
47
+ async function updateTask() {
48
+ const filePath = path.join(aiDir, 'current-task.md')
49
+ const existingContent = await requireFile(filePath)
50
+ let nextContent = existingContent
51
+
52
+ const feature = getArgValue(args, 'feature')
53
+ const goal = getArgValue(args, 'goal')
54
+ const status = getArgValue(args, 'status')
55
+ const version = getArgValue(args, 'version')
56
+ const started = getArgValue(args, 'started')
57
+ const ready = getArgValue(args, 'ready')
58
+
59
+ if (feature) {
60
+ nextContent = replaceMarkdownSection(nextContent, 'Feature Name', feature)
61
+ }
62
+
63
+ if (goal) {
64
+ nextContent = replaceMarkdownSection(nextContent, 'Goal', goal)
65
+ }
66
+
67
+ if (status) {
68
+ nextContent = replaceLabeledValue(nextContent, 'Status', status)
69
+ }
70
+
71
+ if (version) {
72
+ nextContent = replaceLabeledValue(nextContent, 'Version', version)
73
+ }
74
+
75
+ if (started) {
76
+ nextContent = replaceLabeledValue(
77
+ nextContent,
78
+ 'Started',
79
+ started === 'now' ? nowTimestamp() : started,
80
+ )
81
+ }
82
+
83
+ if (ready) {
84
+ nextContent = replaceLabeledValue(nextContent, 'Ready For Archive', ready)
85
+ }
86
+
87
+ const current = getArgList(args, 'current')
88
+ const affected = getArgList(args, 'affected')
89
+ const constraints = getArgList(args, 'constraint')
90
+ const acceptance = getArgList(args, 'accept')
91
+ const checklist = getArgList(args, 'check')
92
+
93
+ if (current.length > 0) {
94
+ nextContent = replaceMarkdownSection(nextContent, 'Current Status', formatBullets(current))
95
+ }
96
+
97
+ if (affected.length > 0) {
98
+ nextContent = replaceMarkdownSection(nextContent, 'Affected Areas', formatBullets(affected))
99
+ }
100
+
101
+ if (constraints.length > 0) {
102
+ nextContent = replaceMarkdownSection(nextContent, 'Constraints', formatBullets(constraints))
103
+ }
104
+
105
+ if (acceptance.length > 0) {
106
+ nextContent = replaceMarkdownSection(
107
+ nextContent,
108
+ 'Acceptance Criteria',
109
+ formatBullets(acceptance),
110
+ )
111
+ }
112
+
113
+ if (checklist.length > 0) {
114
+ nextContent = replaceMarkdownSection(
115
+ nextContent,
116
+ 'Completion Checklist',
117
+ formatChecklist(checklist),
118
+ )
119
+ }
120
+
121
+ await writeTextFile(filePath, nextContent)
122
+ process.stderr.write(`Updated ${filePath}\n`)
123
+ }
124
+
125
+ async function updateHandoff() {
126
+ const filePath = path.join(aiDir, 'session-handoff.md')
127
+ const existingContent = await requireFile(filePath)
128
+ let nextContent = existingContent
129
+
130
+ const updated = getArgValue(args, 'updated')
131
+
132
+ if (updated || Object.keys(args).length > 0) {
133
+ nextContent = replaceMarkdownSection(
134
+ nextContent,
135
+ 'Last Update',
136
+ updated === undefined || updated === 'now' ? nowTimestamp() : updated,
137
+ )
138
+ }
139
+
140
+ const done = getArgList(args, 'done')
141
+ const state = getArgList(args, 'state')
142
+ const next = getArgList(args, 'next')
143
+ const issues = getArgList(args, 'issue')
144
+ const notes = getArgList(args, 'note')
145
+
146
+ if (done.length > 0) {
147
+ nextContent = replaceMarkdownSection(nextContent, 'What Was Done', formatBullets(done))
148
+ }
149
+
150
+ if (state.length > 0) {
151
+ nextContent = replaceMarkdownSection(nextContent, 'Current State', formatBullets(state))
152
+ }
153
+
154
+ if (next.length > 0) {
155
+ nextContent = replaceMarkdownSection(nextContent, 'Next Steps', formatBullets(next))
156
+ }
157
+
158
+ if (issues.length > 0) {
159
+ nextContent = replaceMarkdownSection(nextContent, 'Known Issues', formatBullets(issues))
160
+ }
161
+
162
+ if (notes.length > 0) {
163
+ nextContent = replaceMarkdownSection(nextContent, 'Notes', formatBullets(notes))
164
+ }
165
+
166
+ await writeTextFile(filePath, nextContent)
167
+ process.stderr.write(`Updated ${filePath}\n`)
168
+ }
169
+
170
+ async function appendWorkLog() {
171
+ const filePath = path.join(aiDir, 'work-log.md')
172
+ const existingContent = await requireFile(filePath)
173
+ const timestamp = getArgValue(args, 'timestamp')
174
+ const messages = getArgList(args, 'message')
175
+
176
+ if (messages.length === 0) {
177
+ throw new Error('ai:update:log requires at least one --message value.')
178
+ }
179
+
180
+ const nextContent = `${existingContent.trimEnd()}\n\n## ${
181
+ timestamp === undefined || timestamp === 'now' ? nowTimestamp() : timestamp
182
+ }\n\n${formatBullets(messages)}\n`
183
+ await writeTextFile(filePath, nextContent)
184
+ process.stderr.write(`Updated ${filePath}\n`)
185
+ }
186
+
187
+ async function appendDecision() {
188
+ const filePath = path.join(aiDir, 'decisions.md')
189
+ const existingContent = await requireFile(filePath)
190
+ const timestamp = getArgValue(args, 'timestamp')
191
+ const title = getArgValue(args, 'title')
192
+ const decision = getArgList(args, 'decision')
193
+ const reason = getArgList(args, 'reason')
194
+ const impact = getArgList(args, 'impact')
195
+
196
+ if (decision.length === 0 || reason.length === 0 || impact.length === 0) {
197
+ throw new Error('ai:update:decision requires --decision, --reason, and --impact values.')
198
+ }
199
+
200
+ const heading = title
201
+ ? `## ${timestamp === undefined || timestamp === 'now' ? nowTimestamp() : timestamp} (${title})`
202
+ : `## ${timestamp === undefined || timestamp === 'now' ? nowTimestamp() : timestamp}`
203
+
204
+ const block = `${heading}\n\nDecision:\n${formatBullets(decision)}\n\nReason:\n${formatBullets(reason)}\n\nImpact:\n${formatBullets(impact)}\n`
205
+ const nextContent = existingContent.replace(/^# Decisions\r?\n\r?\n/, `# Decisions\n\n${block}\n`)
206
+
207
+ await writeTextFile(filePath, nextContent)
208
+ process.stderr.write(`Updated ${filePath}\n`)
209
+ }
210
+
211
+ async function appendChangedFile() {
212
+ const filePath = path.join(aiDir, 'changed-files.md')
213
+ const existingContent = await requireFile(filePath)
214
+ const changedFile = getArgValue(args, 'file')
215
+ const notes = getArgList(args, 'note')
216
+
217
+ if (!changedFile || notes.length === 0) {
218
+ throw new Error('ai:update:changed requires --file and at least one --note value.')
219
+ }
220
+
221
+ const block = `\n[${changedFile}]\n${formatBullets(notes)}\n`
222
+ const nextContent = `${existingContent.trimEnd()}${block}`
223
+
224
+ await writeTextFile(filePath, nextContent)
225
+ process.stderr.write(`Updated ${filePath}\n`)
226
+ }
227
+
228
+ async function requireFile(filePath) {
229
+ const content = await readTextIfExists(filePath)
230
+
231
+ if (content === null) {
232
+ throw new Error(`Missing required file: ${filePath}`)
233
+ }
234
+
235
+ return content
236
+ }
@@ -0,0 +1,23 @@
1
+ import path from 'node:path'
2
+
3
+ import { ensureAgentMemory } from './agent-memory-lib.mjs'
4
+ import { writeAceBanner } from './ai-memory-utils.mjs'
5
+
6
+ const targetDir = process.argv[2] ? path.resolve(process.cwd(), process.argv[2]) : process.cwd()
7
+
8
+ writeAceBanner()
9
+
10
+ const result = await ensureAgentMemory(targetDir)
11
+
12
+ if (result.createdFiles.length === 0 && result.updatedFiles.length === 0) {
13
+ process.stderr.write(`ACE memory is already up to date in ${targetDir}\n`)
14
+ process.exit(0)
15
+ }
16
+
17
+ if (result.updatedFiles.length > 0) {
18
+ process.stderr.write(`Updated: ${result.updatedFiles.join(', ')}\n`)
19
+ }
20
+
21
+ if (result.createdFiles.length > 0) {
22
+ process.stderr.write(`Created: ${result.createdFiles.join(', ')}\n`)
23
+ }
@@ -0,0 +1,22 @@
1
+ import path from 'node:path'
2
+
3
+ import { validateAgentMemory } from './agent-memory-lib.mjs'
4
+ import { writeAceBanner } from './ai-memory-utils.mjs'
5
+
6
+ const targetDir = process.argv[2] ? path.resolve(process.cwd(), process.argv[2]) : process.cwd()
7
+
8
+ writeAceBanner()
9
+
10
+ const issues = await validateAgentMemory(targetDir)
11
+
12
+ if (issues.length > 0) {
13
+ process.stderr.write(`ACE memory check failed for ${targetDir}\n`)
14
+
15
+ for (const issue of issues) {
16
+ process.stderr.write(`- ${issue}\n`)
17
+ }
18
+
19
+ process.exit(1)
20
+ }
21
+
22
+ process.stderr.write(`ACE memory check passed for ${targetDir}\n`)