code-foundry 1.13.0 → 1.14.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.14.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.13.0...v1.14.0) (2026-09-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** add shared validation plans and agent check evidence ([#538](https://github.com/0xPlayerOne/code-foundry/issues/538)) ([95af9ed](https://github.com/0xPlayerOne/code-foundry/commit/95af9ed42682ff3ed1dc74609ea4628d0c95a799))
9
+
3
10
  ## [1.13.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.12.0...v1.13.0) (2026-09-09)
4
11
 
5
12
 
@@ -16,8 +16,8 @@ belong to the run that produced them, not the source tree.
16
16
  | Runtime dependencies | 0 | Keep the installed CLI dependency-free |
17
17
  | Development dependencies | 4 | Prevent unreviewed toolchain growth |
18
18
  | Packed artifact | 210 kB | Bound registry transfer and install cost |
19
- | Unpacked artifact | 800 kB | Bound installed footprint |
20
- | Packed files | 90 | Detect accidental release contents |
19
+ | Unpacked artifact | 820 kB | Bound installed footprint |
20
+ | Packed files | 92 | Detect accidental release contents |
21
21
 
22
22
  The performance workflow disables build-cache reads and writes for this task.
23
23
  That makes timing comparisons independent of a warm protected-branch cache and
package/docs/README.md CHANGED
@@ -14,6 +14,7 @@ its own names, environments, and deployment details.
14
14
  - [Caching and remote caching](CACHING.md)
15
15
  - [Performance budgets and baselines](PERFORMANCE.md)
16
16
  - [Required capabilities and task evidence](required-capabilities.md)
17
+ - [Agent-facing validation commands](agent-validation.md)
17
18
  - [Declarative fleet inventory and staged rollouts](fleet-rollouts.md)
18
19
 
19
20
  ## Repository-specific documentation
@@ -0,0 +1,94 @@
1
+ # Agent-facing validation commands
2
+
3
+ The public CLI now exposes a stable plan/check interface using the same discovery,
4
+ required-capability policy, ecosystem executor, and task receipts as reusable CI.
5
+
6
+ ```sh
7
+ code-foundry plan --changed --base origin/main --json
8
+ code-foundry check --tier fast --json
9
+ code-foundry check --tier audit --json
10
+ ```
11
+
12
+ Both commands accept `--target PATH`. Use the Code Foundry version pinned by the
13
+ repository, not an unreviewed floating installation. `plan` executes discovery
14
+ only: it does not install dependencies, run project checks, modify source files,
15
+ or emit skip-receipt files. Required entrypoints are validated across the complete
16
+ task set, including tasks deferred from the selected local tier.
17
+
18
+ ## Tiers and change awareness
19
+
20
+ `fast` selects formatting, linting, type checking, build, unit tests, and performance.
21
+ `audit` adds integration, E2E, and smoke tests. Ecosystem commands and applicability
22
+ come from the shared runtime; the CLI does not invent alternate test commands.
23
+ Repository-owned scripts remain authoritative. Missing required tasks fail
24
+ planning instead of returning a misleading successful subset.
25
+
26
+ `--changed` includes paths changed from the selected base, staged changes,
27
+ unstaged changes, and untracked files. Committed paths use the merge-base with
28
+ that ref, so an independently advanced base branch does not make base-only
29
+ changes look like head changes. `--base` defaults to HEAD and requires `--changed`.
30
+ Invalid or unavailable base revisions fail rather than being treated as no changes.
31
+ Paths use Git's NUL-separated format, preserving whitespace and unusual filenames.
32
+ Generated `.code-foundry/` evidence is excluded from change annotations. Change awareness is **annotation-only**: without a verified dependency
33
+ graph it does not skip required tasks or assume a documentation change cannot
34
+ affect a custom command.
35
+
36
+ Local tiers are not a replacement for the complete GitHub Validation / Gate.
37
+ Security scans, CodeQL, release-diff policy where applicable, protected-environment
38
+ checks, and required reviews remain separate. Every plan/result explicitly marks
39
+ remote validation as required. A fast result also names deferred local tasks.
40
+ A local audit result is not permission to mark a PR ready or merge it.
41
+
42
+ ## Execution and evidence
43
+
44
+ Install the repository's locked dependencies through its normal setup process
45
+ before running checks. This CLI does not add an implicit install step or grant
46
+ credentials. It runs trusted repository scripts in the existing environment;
47
+ those scripts are not sandboxed and retain their normal tool/network behavior.
48
+ The formatter/runtime's existing semantics are unchanged: a configured command
49
+ that rewrites files still rewrites files. Prefer check-only scripts in CI.
50
+
51
+ Task stdout/stderr goes to stderr, keeping `--json` stdout parseable. Each selected
52
+ applicable task runs through the public runtime. A zero exit without a fresh,
53
+ matching, successful task receipt fails. A failed task blocks remaining applicable
54
+ tasks; optional inapplicable tasks are reported as skipped. An entirely skipped
55
+ check is labeled `skipped`, never `passed`.
56
+
57
+ A run stores `.code-foundry/agent-results/check-*/summary.json` and snapshots of
58
+ its task receipts. The result includes the source commit, dirty-tree indication,
59
+ selected/deferred tasks, reasons, exit codes, signals, receipt paths, and repository
60
+ artifact paths such as coverage or browser evidence. HEAD identifies the base
61
+ commit; a dirty working tree is not a cryptographically identified immutable
62
+ source snapshot. Reports are local evidence, not signed provenance.
63
+
64
+ The aggregate and task receipts are retained per run so later checks do not
65
+ overwrite the earlier JSON evidence. Artifact paths inside receipts point to
66
+ repository-owned files and may be overwritten by subsequent test runs; copy them
67
+ into a CI artifact for long-term retention. Do not upload the whole hidden
68
+ repository tree or secrets. The CLI neither captures environment variables into
69
+ reports nor grants publish/merge/deploy access.
70
+
71
+ An exclusive local `agent-results/active.lock` prevents overlapping agent checks
72
+ from confusing receipts. After a killed process, inspect the checkout and confirm
73
+ there is no active check before manually removing a stale lock. Normal completion
74
+ and failures clean the lock. This is a local CLI lock, not a distributed lock
75
+ against every other tool writing into the repository.
76
+
77
+ `--timeout SECONDS` bounds each check subprocess (default 600, range 1–3600).
78
+ A timeout or missing receipt fails. Project scripts remain responsible for
79
+ cleaning up their own servers and subprocesses. Plans do not accept an execution
80
+ timeout. Unknown, duplicate, or ignored arguments fail rather than silently alter
81
+ validation behavior.
82
+
83
+ ## Integration and tests
84
+
85
+ This change depends on the required-capabilities/task-evidence runtime change.
86
+ It deliberately imports shared policy instead of maintaining a competing copy.
87
+ Merge that prerequisite before this CLI change and retain `runtime-core.mjs` when
88
+ vendoring the runtime.
89
+
90
+ Run `node --test test/agent-check.test.mjs`. Tests exercise real Git change
91
+ inspection and the actual policy/evidence wrapper with a deterministic ecosystem
92
+ executor, including read-only planning, required deferred tasks, failed commands,
93
+ stale evidence, receipt snapshots, locking, and separation of JSON from task logs.
94
+ Full ecosystem dependencies and GitHub-side checks still need their own CI run.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
6
6
  "bugs": {
package/src/cli.mjs CHANGED
@@ -16,6 +16,8 @@ Usage:
16
16
  npx code-foundry init [--target PATH]
17
17
  npx code-foundry sync [--target PATH]
18
18
  npx code-foundry doctor [--target PATH]
19
+ npx code-foundry plan [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--json]
20
+ npx code-foundry check [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--timeout SECONDS] [--json]
19
21
  npx code-foundry ci pause|resume|status [--target PATH]
20
22
  npx code-foundry release reconcile [--github] [--base BRANCH] [--head BRANCH]
21
23
  npx code-foundry release hook --tag TAG --workflow WORKFLOW
@@ -124,6 +126,11 @@ function parseArgs(argv) {
124
126
  }
125
127
 
126
128
  async function main() {
129
+ if (['plan', 'check'].includes(process.argv[2])) {
130
+ const { agentCommand } = await import('./commands/agent-check.mjs')
131
+ process.exitCode = agentCommand(process.argv.slice(2))
132
+ return
133
+ }
127
134
  const { command, options } = parseArgs(process.argv.slice(2))
128
135
  const target = resolve(options.target)
129
136
 
@@ -0,0 +1,309 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from 'node:fs'
12
+ import { join, resolve } from 'node:path'
13
+ import { spawnSync } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { taskProfile } from '../runtime.mjs'
16
+ import { evidencePath, fingerprint, TASKS } from '../lib/task-policy.mjs'
17
+
18
+ const runtime = fileURLToPath(new URL('../runtime.mjs', import.meta.url))
19
+ const fastTasks = new Set(['format', 'lint', 'type_check', 'build', 'unit', 'performance'])
20
+ export const agentUsage = `code-foundry plan [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--json]
21
+ code-foundry check [--target PATH] [--tier fast|audit] [--changed] [--base REF] [--timeout SECONDS] [--json]
22
+
23
+ plan discovers tasks without executing them. check invokes the shared CI runtime.
24
+ --changed annotates affected paths; it never prunes required or custom checks.
25
+ fast includes CI tasks, unit tests, and performance; audit adds integration/E2E/smoke.
26
+ Neither command substitutes for GitHub Security, CodeQL, release policy, or review.
27
+ `
28
+
29
+ /** @typedef {{command: 'plan'|'check', target: string, tier: 'fast'|'audit', changed: boolean, base: string, json: boolean, timeout: number}} AgentOptions */
30
+ /** @typedef {{runtime?: string, core?: string}} RuntimePaths */
31
+
32
+ /** @param {string[]} argv @returns {AgentOptions} */
33
+ export function parseAgentArgs(argv) {
34
+ const [command, ...args] = argv
35
+ if (command !== 'plan' && command !== 'check') throw new Error('Expected plan or check')
36
+ /** @type {AgentOptions} */
37
+ const options = {
38
+ command,
39
+ target: process.cwd(),
40
+ tier: 'fast',
41
+ changed: false,
42
+ base: 'HEAD',
43
+ json: false,
44
+ timeout: 600000,
45
+ }
46
+ const seen = new Set()
47
+ while (args.length) {
48
+ const key = args.shift()
49
+ if (!key || seen.has(key)) throw new Error(`Duplicate or missing option: ${key}`)
50
+ seen.add(key)
51
+ if (key === '--json') options.json = true
52
+ else if (key === '--changed') options.changed = true
53
+ else {
54
+ if (!['--target', '--tier', '--base', '--timeout'].includes(key))
55
+ throw new Error(`Unknown option: ${key}`)
56
+ const value = args.shift()
57
+ if (!value || value.startsWith('-')) throw new Error(`Missing value for ${key}`)
58
+ if (key === '--target') options.target = resolve(value)
59
+ if (key === '--base') options.base = value
60
+ if (key === '--tier') {
61
+ if (!['fast', 'audit'].includes(value)) throw new Error('tier must be fast or audit')
62
+ options.tier = /** @type {'fast'|'audit'} */ (value)
63
+ }
64
+ if (key === '--timeout') {
65
+ const seconds = Number(value)
66
+ if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 3600)
67
+ throw new Error('timeout must be 1–3600 whole seconds')
68
+ options.timeout = seconds * 1000
69
+ }
70
+ }
71
+ }
72
+ if (seen.has('--base') && !options.changed) throw new Error('--base requires --changed')
73
+ if (command === 'plan' && seen.has('--timeout'))
74
+ throw new Error('--timeout applies only to check execution')
75
+ return options
76
+ }
77
+
78
+ /** @param {string} root @param {string[]} args */
79
+ function git(root, args) {
80
+ const result = spawnSync('git', args, {
81
+ cwd: root,
82
+ encoding: 'utf8',
83
+ timeout: 10000,
84
+ maxBuffer: 8 * 1024 * 1024,
85
+ })
86
+ if (result.error || result.status !== 0)
87
+ throw new Error(`Unable to inspect repository: git ${args[0]}`)
88
+ return result.stdout
89
+ }
90
+
91
+ /** @param {string} value */
92
+ function hasInvalidBaseRefCharacter(value) {
93
+ return [...value].some((character) => {
94
+ const code = character.codePointAt(0) ?? 0
95
+ return code <= 0x20 || code === 0x7f
96
+ })
97
+ }
98
+
99
+ /** @param {AgentOptions} options */
100
+ export function sourceContext(options) {
101
+ const sourceSha = git(options.target, ['rev-parse', '--verify', 'HEAD']).trim()
102
+ if (!/^[0-9a-f]{40}$/i.test(sourceSha))
103
+ throw new Error('Repository HEAD must resolve to a commit')
104
+ const dirty = git(options.target, ['status', '--porcelain=v1', '-z'])
105
+ .split('\0')
106
+ .filter(Boolean)
107
+ .some((line) => !line.slice(3).startsWith('.code-foundry/'))
108
+ let baseSha = null
109
+ /** @type {string[]} */
110
+ let changedFiles = []
111
+ if (options.changed) {
112
+ if (options.base.startsWith('-') || hasInvalidBaseRefCharacter(options.base))
113
+ throw new Error('Invalid base ref')
114
+ baseSha = git(options.target, ['rev-parse', '--verify', `${options.base}^{commit}`]).trim()
115
+ const lists = [
116
+ git(options.target, ['diff', '--name-only', '-z', `${baseSha}...HEAD`, '--']),
117
+ git(options.target, ['diff', '--cached', '--name-only', '-z', '--']),
118
+ git(options.target, ['diff', '--name-only', '-z', '--']),
119
+ git(options.target, ['ls-files', '--others', '--exclude-standard', '-z']),
120
+ ]
121
+ changedFiles = [
122
+ ...new Set(
123
+ lists
124
+ .flatMap((value) => value.split('\0'))
125
+ .filter((file) => file && !file.startsWith('.code-foundry/'))
126
+ ),
127
+ ]
128
+ // This freshly created array is local to the plan.
129
+ // oxlint-disable-next-line unicorn/no-array-sort
130
+ changedFiles.sort()
131
+ }
132
+ return { sourceSha, dirty, baseSha, changedFiles }
133
+ }
134
+
135
+ /** @param {AgentOptions} options @param {RuntimePaths} [paths] */
136
+ export function planChecks(options, paths = {}) {
137
+ const source = sourceContext(options)
138
+ // Direct discovery shares CI policy, but unlike ci task_profile it writes no skip receipts.
139
+ const tasks = TASKS.map((task) => ({
140
+ ...taskProfile(options.target, task, paths.core),
141
+ selected: options.tier === 'audit' || fastTasks.has(task),
142
+ argv: [process.execPath, paths.runtime ?? runtime, 'ci', task],
143
+ }))
144
+ return {
145
+ schemaVersion: 1,
146
+ kind: 'code-foundry-validation-plan',
147
+ status: 'planned',
148
+ tier: options.tier,
149
+ ...source,
150
+ tasks,
151
+ changePolicy: 'annotation-only; no tasks pruned without a proven dependency graph',
152
+ remoteValidationRequired: true,
153
+ deferredRemoteChecks: [
154
+ 'Security',
155
+ 'CodeQL',
156
+ 'release policy when applicable',
157
+ 'required reviews',
158
+ ],
159
+ }
160
+ }
161
+
162
+ /** @param {string} root */
163
+ function outputDirectory(root) {
164
+ let directory = resolve(root)
165
+ for (const component of ['.code-foundry', 'agent-results']) {
166
+ directory = join(directory, component)
167
+ if (!existsSync(directory)) mkdirSync(directory)
168
+ if (lstatSync(directory).isSymbolicLink() || !lstatSync(directory).isDirectory())
169
+ throw new Error('Agent evidence directory must be a real directory inside the repository')
170
+ }
171
+ return directory
172
+ }
173
+
174
+ /** @param {AgentOptions} options @param {RuntimePaths} [paths] */
175
+ export function checkRepository(options, paths = {}) {
176
+ const plan = planChecks(options, paths)
177
+ const directory = outputDirectory(options.target)
178
+ const lock = join(directory, 'active.lock')
179
+ try {
180
+ mkdirSync(lock)
181
+ } catch {
182
+ throw new Error(
183
+ 'Another agent check may be running; inspect agent-results/active.lock before recovery'
184
+ )
185
+ }
186
+ let runDirectory
187
+ try {
188
+ runDirectory = mkdtempSync(join(directory, 'check-'))
189
+ } catch (error) {
190
+ try {
191
+ rmSync(lock, { recursive: true, force: true })
192
+ } catch {
193
+ // Preserve the original temporary-directory error.
194
+ }
195
+ throw error
196
+ }
197
+ const reportFile = join(runDirectory, 'summary.json')
198
+ /** @type {Record<string, any>} */
199
+ const report = {
200
+ ...plan,
201
+ kind: 'code-foundry-validation-result',
202
+ status: 'failed',
203
+ startedAt: new Date().toISOString(),
204
+ completedAt: null,
205
+ tasks: [],
206
+ report: reportFile,
207
+ deferredTasks: plan.tasks.filter((task) => !task.selected).map((task) => task.task),
208
+ }
209
+ let failed = false
210
+ try {
211
+ for (const item of plan.tasks.filter((task) => task.selected)) {
212
+ if (!item.applicable || failed) {
213
+ report.tasks.push({
214
+ ...item,
215
+ status: failed && item.applicable ? 'blocked' : 'skipped',
216
+ reason: failed && item.applicable ? 'an earlier task failed' : item.reason,
217
+ })
218
+ continue
219
+ }
220
+ const receiptFile = evidencePath(options.target, `.code-foundry/results/${item.task}.json`)
221
+ const before = fingerprint(receiptFile)
222
+ const result = spawnSync(item.argv[0], item.argv.slice(1), {
223
+ cwd: options.target,
224
+ timeout: options.timeout,
225
+ // Keep stdout exclusively available for the public JSON contract.
226
+ stdio: ['ignore', 2, 2],
227
+ env: { ...process.env, GITHUB_OUTPUT: '', GITHUB_STEP_SUMMARY: '' },
228
+ })
229
+ let receipt = null
230
+ let reason = result.error
231
+ ? `Runtime execution failed: ${/** @type {NodeJS.ErrnoException} */ (result.error).code ?? result.error.name}`
232
+ : ''
233
+ if (fingerprint(receiptFile) !== before && existsSync(receiptFile)) {
234
+ try {
235
+ receipt = JSON.parse(readFileSync(receiptFile, 'utf8'))
236
+ if (
237
+ receipt.kind !== 'code-foundry-task-result' ||
238
+ receipt.schemaVersion !== 1 ||
239
+ receipt.task !== item.task ||
240
+ receipt.sourceSha !== plan.sourceSha
241
+ )
242
+ throw new Error('Receipt identity does not match the planned task/source')
243
+ } catch (error) {
244
+ reason = error instanceof Error ? error.message : String(error)
245
+ receipt = null
246
+ }
247
+ }
248
+ const passed = result.status === 0 && receipt?.status === 'passed' && !reason
249
+ const snapshot = receipt ? join(runDirectory, `${item.task}.json`) : null
250
+ if (snapshot) writeFileSync(snapshot, `${JSON.stringify(receipt, null, 2)}\n`)
251
+ failed = !passed
252
+ report.tasks.push({
253
+ ...item,
254
+ status: passed ? 'passed' : 'failed',
255
+ exitCode: result.status,
256
+ signal: result.signal,
257
+ reason: reason || receipt?.reason || 'missing fresh successful task evidence',
258
+ receipt: snapshot,
259
+ artifacts: receipt?.artifacts ?? [],
260
+ })
261
+ }
262
+ report.status = failed
263
+ ? 'failed'
264
+ : report.tasks.some((/** @type {any} */ task) => task.status === 'passed')
265
+ ? 'passed'
266
+ : 'skipped'
267
+ return report
268
+ } finally {
269
+ report.completedAt = new Date().toISOString()
270
+ try {
271
+ writeFileSync(reportFile, `${JSON.stringify(report, null, 2)}\n`)
272
+ } finally {
273
+ rmSync(lock, { recursive: true })
274
+ }
275
+ }
276
+ }
277
+
278
+ /** @param {string[]} argv */
279
+ export function agentCommand(argv) {
280
+ const jsonOutput = argv.includes('--json')
281
+ if (argv.includes('--help') || argv.includes('-h')) {
282
+ console.log(agentUsage)
283
+ return 0
284
+ }
285
+ try {
286
+ const options = parseAgentArgs(argv)
287
+ const result = options.command === 'plan' ? planChecks(options) : checkRepository(options)
288
+ if (options.json) console.log(JSON.stringify(result, null, 2))
289
+ else {
290
+ console.log(`${options.command}: ${result.status} (${options.tier}, ${result.sourceSha})`)
291
+ for (const item of result.tasks)
292
+ console.log(
293
+ `${item.task}: ${item.status ?? (item.selected ? (item.applicable ? 'planned' : 'skipped') : 'deferred')} — ${item.reason}`
294
+ )
295
+ console.log('GitHub security checks and review requirements remain separate.')
296
+ }
297
+ return result.status === 'failed' ? 1 : 0
298
+ } catch (error) {
299
+ const result = {
300
+ schemaVersion: 1,
301
+ kind: 'code-foundry-validation-error',
302
+ status: 'failed',
303
+ reason: error instanceof Error ? error.message : String(error),
304
+ }
305
+ if (jsonOutput) console.log(JSON.stringify(result, null, 2))
306
+ else console.error(result.reason)
307
+ return 1
308
+ }
309
+ }