@aperant/framework 0.9.0 → 0.11.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.
Files changed (138) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/agents/apt-verifier.md +6 -3
  3. package/dist/cli/commands/catch-up.d.mts +9 -0
  4. package/dist/cli/commands/catch-up.d.mts.map +1 -0
  5. package/dist/cli/commands/catch-up.mjs +64 -0
  6. package/dist/cli/commands/catch-up.mjs.map +1 -0
  7. package/dist/cli/commands/init.d.mts.map +1 -1
  8. package/dist/cli/commands/init.mjs +17 -1
  9. package/dist/cli/commands/init.mjs.map +1 -1
  10. package/dist/cli/commands/pr-review.d.mts +16 -4
  11. package/dist/cli/commands/pr-review.d.mts.map +1 -1
  12. package/dist/cli/commands/pr-review.mjs +30 -11
  13. package/dist/cli/commands/pr-review.mjs.map +1 -1
  14. package/dist/cli/commands/route.d.mts.map +1 -1
  15. package/dist/cli/commands/route.mjs +92 -2
  16. package/dist/cli/commands/route.mjs.map +1 -1
  17. package/dist/cli/commands/validate-evidence.d.mts +1 -1
  18. package/dist/cli/config/share-policy.d.mts.map +1 -1
  19. package/dist/cli/config/share-policy.mjs +1 -0
  20. package/dist/cli/config/share-policy.mjs.map +1 -1
  21. package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
  22. package/dist/cli/config/upgrade-gitignore.mjs +1 -0
  23. package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
  24. package/dist/cli/consistency/parse-review.mjs +5 -3
  25. package/dist/cli/consistency/parse-review.mjs.map +1 -1
  26. package/dist/cli/consistency/rules/r5-verdict-consistency.d.mts.map +1 -1
  27. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs +10 -8
  28. package/dist/cli/consistency/rules/r5-verdict-consistency.mjs.map +1 -1
  29. package/dist/cli/coordination/catch-up.d.mts +26 -0
  30. package/dist/cli/coordination/catch-up.d.mts.map +1 -0
  31. package/dist/cli/coordination/catch-up.mjs +239 -0
  32. package/dist/cli/coordination/catch-up.mjs.map +1 -0
  33. package/dist/cli/coordination/last-seen.d.mts +45 -0
  34. package/dist/cli/coordination/last-seen.d.mts.map +1 -0
  35. package/dist/cli/coordination/last-seen.mjs +128 -0
  36. package/dist/cli/coordination/last-seen.mjs.map +1 -0
  37. package/dist/cli/coordination/store.d.mts +15 -0
  38. package/dist/cli/coordination/store.d.mts.map +1 -1
  39. package/dist/cli/coordination/store.mjs +16 -0
  40. package/dist/cli/coordination/store.mjs.map +1 -1
  41. package/dist/cli/design/frontmatter-schema.d.mts +8 -8
  42. package/dist/cli/dispatch.d.mts.map +1 -1
  43. package/dist/cli/dispatch.mjs +2 -0
  44. package/dist/cli/dispatch.mjs.map +1 -1
  45. package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
  46. package/dist/cli/gate/gates/review-clean.mjs +4 -2
  47. package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
  48. package/dist/cli/git/default-branch.d.mts +51 -0
  49. package/dist/cli/git/default-branch.d.mts.map +1 -0
  50. package/dist/cli/git/default-branch.mjs +234 -0
  51. package/dist/cli/git/default-branch.mjs.map +1 -0
  52. package/dist/cli/git/identity.d.mts +3 -5
  53. package/dist/cli/git/identity.d.mts.map +1 -1
  54. package/dist/cli/git/identity.mjs +10 -4
  55. package/dist/cli/git/identity.mjs.map +1 -1
  56. package/dist/cli/install/legacy-paths.d.mts.map +1 -1
  57. package/dist/cli/install/legacy-paths.mjs +2 -0
  58. package/dist/cli/install/legacy-paths.mjs.map +1 -1
  59. package/dist/cli/install/mcp-provision.d.mts +26 -0
  60. package/dist/cli/install/mcp-provision.d.mts.map +1 -0
  61. package/dist/cli/install/mcp-provision.mjs +281 -0
  62. package/dist/cli/install/mcp-provision.mjs.map +1 -0
  63. package/dist/cli/install/mcp-server-specs.d.mts +80 -0
  64. package/dist/cli/install/mcp-server-specs.d.mts.map +1 -0
  65. package/dist/cli/install/mcp-server-specs.mjs +102 -0
  66. package/dist/cli/install/mcp-server-specs.mjs.map +1 -0
  67. package/dist/cli/install/runtime-detect.d.mts +1 -0
  68. package/dist/cli/install/runtime-detect.d.mts.map +1 -1
  69. package/dist/cli/install/runtime-detect.mjs +5 -0
  70. package/dist/cli/install/runtime-detect.mjs.map +1 -1
  71. package/dist/cli/install/toml-merge.d.mts +40 -0
  72. package/dist/cli/install/toml-merge.d.mts.map +1 -0
  73. package/dist/cli/install/toml-merge.mjs +134 -0
  74. package/dist/cli/install/toml-merge.mjs.map +1 -0
  75. package/dist/cli/personas/sidecar.d.mts +1 -1
  76. package/dist/cli/roadmap/lifecycle.d.mts +1 -1
  77. package/dist/cli/roadmap/rollup.d.mts +2 -2
  78. package/dist/cli/util/events-path.d.mts +8 -0
  79. package/dist/cli/util/events-path.d.mts.map +1 -0
  80. package/dist/cli/util/events-path.mjs +23 -0
  81. package/dist/cli/util/events-path.mjs.map +1 -0
  82. package/dist/cli/util/runtime-capabilities.d.mts +30 -0
  83. package/dist/cli/util/runtime-capabilities.d.mts.map +1 -0
  84. package/dist/cli/util/runtime-capabilities.mjs +214 -0
  85. package/dist/cli/util/runtime-capabilities.mjs.map +1 -0
  86. package/dist/cli/verify-proof/audit.d.mts +2 -7
  87. package/dist/cli/verify-proof/audit.d.mts.map +1 -1
  88. package/dist/cli/verify-proof/audit.mjs +11 -17
  89. package/dist/cli/verify-proof/audit.mjs.map +1 -1
  90. package/dist/cli/verify-proof/runtime-detect.d.mts +2 -29
  91. package/dist/cli/verify-proof/runtime-detect.d.mts.map +1 -1
  92. package/dist/cli/verify-proof/runtime-detect.mjs +8 -203
  93. package/dist/cli/verify-proof/runtime-detect.mjs.map +1 -1
  94. package/dist/plugin/.claude-plugin/plugin.json +2 -1
  95. package/dist/plugin/agents/apt-verifier.md +6 -3
  96. package/dist/plugin/skills/apt/SKILL.md +29 -0
  97. package/dist/plugin/skills/apt-catch-up/SKILL.md +79 -0
  98. package/dist/plugin/skills/apt-pr-review/SKILL.md +59 -4
  99. package/dist/plugin/skills/apt-setup/SKILL.md +76 -0
  100. package/dist/plugin/skills/apt-verify/SKILL.md +1 -1
  101. package/dist/plugin/skills/apt-verify-proof/SKILL.md +25 -1
  102. package/drivers/browser/skill/agent-browser/SKILL.md +51 -0
  103. package/drivers/tauri/README.md +107 -0
  104. package/drivers/tauri/driver.mjs +88 -0
  105. package/drivers/tauri/manifest.json +37 -0
  106. package/drivers/tauri/scripts/tauri-mcp.sh +41 -0
  107. package/package.json +138 -138
  108. package/skills/apt/SKILL.md +29 -0
  109. package/skills/apt-catch-up/SKILL.md +79 -0
  110. package/skills/apt-pr-review/SKILL.md +59 -4
  111. package/skills/apt-setup/SKILL.md +76 -0
  112. package/skills/apt-verify/SKILL.md +1 -1
  113. package/skills/apt-verify-proof/SKILL.md +25 -1
  114. package/src/cli/commands/catch-up.mjs +67 -0
  115. package/src/cli/commands/init.mjs +19 -0
  116. package/src/cli/commands/pr-review.mjs +32 -11
  117. package/src/cli/commands/route.mjs +92 -1
  118. package/src/cli/config/share-policy.mjs +1 -0
  119. package/src/cli/config/upgrade-gitignore.mjs +1 -0
  120. package/src/cli/consistency/parse-review.mjs +5 -3
  121. package/src/cli/consistency/rules/r5-verdict-consistency.mjs +14 -9
  122. package/src/cli/coordination/catch-up.mjs +231 -0
  123. package/src/cli/coordination/last-seen.mjs +131 -0
  124. package/src/cli/coordination/store.mjs +18 -0
  125. package/src/cli/dispatch.mjs +2 -0
  126. package/src/cli/gate/gates/review-clean.mjs +4 -2
  127. package/src/cli/git/default-branch.mjs +250 -0
  128. package/src/cli/git/identity.mjs +9 -3
  129. package/src/cli/install/legacy-paths.mjs +2 -0
  130. package/src/cli/install/mcp-provision.mjs +293 -0
  131. package/src/cli/install/mcp-server-specs.mjs +101 -0
  132. package/src/cli/install/runtime-detect.mjs +9 -0
  133. package/src/cli/install/toml-merge.mjs +139 -0
  134. package/src/cli/util/events-path.mjs +24 -0
  135. package/src/cli/util/runtime-capabilities.mjs +216 -0
  136. package/src/cli/verify-proof/audit.mjs +11 -17
  137. package/src/cli/verify-proof/runtime-detect.mjs +11 -204
  138. package/templates/config.json +13 -1
@@ -18,6 +18,15 @@ import { existsSync, statSync } from 'node:fs'
18
18
  import { join } from 'node:path'
19
19
  import { MANIFEST_FILENAME } from './manifest.mjs'
20
20
 
21
+ // Package.json-shape capability detection lives in util/ (shared with
22
+ // verify-proof/runtime-detect.mjs). Re-exported here so install-layer callers
23
+ // (e.g. install/mcp-provision.mjs) consume it from their OWN layer instead of
24
+ // reaching across into verify-proof/.
25
+ export {
26
+ detectFromPackageJson,
27
+ loadRuntimeCapabilities,
28
+ } from '../util/runtime-capabilities.mjs'
29
+
21
30
  /**
22
31
  * @typedef {Object} RuntimeDescriptor
23
32
  * @property {string} id Stable identifier (`claude`, `cursor`, …).
@@ -0,0 +1,139 @@
1
+ /**
2
+ * install/toml-merge.mjs — dependency-free minimal TOML upsert for the
3
+ * Codex MCP-server config table.
4
+ *
5
+ * Codex's MCP config lives at project-root `.codex/config.toml` as:
6
+ *
7
+ * [mcp_servers.<id>]
8
+ * command = "bash"
9
+ * args = ["./scripts/tauri-mcp.sh"]
10
+ *
11
+ * [mcp_servers.<id>.env]
12
+ * FOO = "bar"
13
+ *
14
+ * No TOML serializer ships in the framework and `@iarna/toml` is not a
15
+ * dependency, so we hand-roll the MINIMAL upsert covering ONLY this shape
16
+ * (string command, string[] args, optional nested env table of
17
+ * string=string). Presence is detected by scanning for the
18
+ * `[mcp_servers.<id>]` header line; if present, we SKIP unchanged (TOFU
19
+ * per ID-05) — never diff/merge/overwrite a hand-authored table. All other
20
+ * content is preserved verbatim; the new table block is appended at EOF.
21
+ *
22
+ * Pure module — string in, string out.
23
+ */
24
+
25
+ /**
26
+ * Escape a string for a TOML basic (double-quoted) string. Only the
27
+ * characters that can occur in a command/arg/env value: backslash and
28
+ * double-quote.
29
+ *
30
+ * @param {string} s
31
+ * @returns {string}
32
+ */
33
+ function tomlString(s) {
34
+ return `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
35
+ }
36
+
37
+ /**
38
+ * Render a TOML array of double-quoted strings: `["a", "b"]`.
39
+ *
40
+ * @param {string[]} arr
41
+ * @returns {string}
42
+ */
43
+ function tomlStringArray(arr) {
44
+ return `[${arr.map((a) => tomlString(a)).join(', ')}]`
45
+ }
46
+
47
+ /**
48
+ * Render a TOML key. A bare key (TOML `[A-Za-z0-9_-]+`) is emitted verbatim;
49
+ * any other key is quoted as a TOML basic string so a non-bare env name
50
+ * (e.g. one containing `.`, a space, or `"`) cannot break the table or inject
51
+ * extra structure.
52
+ *
53
+ * @param {string} key
54
+ * @returns {string}
55
+ */
56
+ function tomlKey(key) {
57
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key)
58
+ }
59
+
60
+ /**
61
+ * Whether `existingToml` already declares a `[mcp_servers.<id>]` table.
62
+ * Matches the header at the start of a line (ignoring leading whitespace),
63
+ * permitting an optional TOML trailing comment (` # …`) after the header —
64
+ * a hand-authored `[mcp_servers.tauri] # my server` must still count as
65
+ * present, otherwise the upsert would append a DUPLICATE table and produce
66
+ * an unparseable config (two same-named tables). The id is taken literally
67
+ * (dotted-key TOML ids do not occur for our server ids — electron/tauri are
68
+ * bare) and regex-escaped defensively. A `#`-prefixed comment LINE that
69
+ * merely mentions the header is NOT a match (the header must be the first
70
+ * non-whitespace token).
71
+ *
72
+ * @param {string} existingToml
73
+ * @param {string} serverId
74
+ * @returns {boolean}
75
+ */
76
+ export function hasMcpServerTable(existingToml, serverId) {
77
+ if (typeof existingToml !== 'string' || existingToml.length === 0) return false
78
+ const escapedId = serverId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
79
+ // `[mcp_servers.<id>]` at line start (after optional indent), optionally
80
+ // followed by whitespace and/or a `#` trailing comment, to EOL.
81
+ const headerRe = new RegExp(`^\\s*\\[mcp_servers\\.${escapedId}\\]\\s*(#.*)?$`)
82
+ const lines = existingToml.split(/\r?\n/)
83
+ for (const line of lines) {
84
+ if (headerRe.test(line)) return true
85
+ }
86
+ return false
87
+ }
88
+
89
+ /**
90
+ * Upsert a `[mcp_servers.<id>]` table into a Codex config.toml string.
91
+ *
92
+ * TOFU (ID-05): if a `[mcp_servers.<id>]` table already exists, returns the
93
+ * input unchanged with `action: 'skipped'`. Otherwise appends the new table
94
+ * (and a nested `[mcp_servers.<id>.env]` table when `env` is non-empty) at
95
+ * EOF, preserving all existing content verbatim, and returns the new
96
+ * content with `action: 'written'`.
97
+ *
98
+ * @param {string} existingToml — current file contents ('' when the file is absent)
99
+ * @param {string} serverId
100
+ * @param {{command: string, args: string[], env?: Record<string, string>}} spec
101
+ * @returns {{content: string, action: 'written'|'skipped'}}
102
+ */
103
+ export function upsertMcpServerToml(existingToml, serverId, spec) {
104
+ const existing = typeof existingToml === 'string' ? existingToml : ''
105
+ if (hasMcpServerTable(existing, serverId)) {
106
+ return { content: existing, action: 'skipped' }
107
+ }
108
+
109
+ const lines = []
110
+ lines.push(`[mcp_servers.${serverId}]`)
111
+ lines.push(`command = ${tomlString(spec.command)}`)
112
+ lines.push(`args = ${tomlStringArray(spec.args ?? [])}`)
113
+
114
+ const env = spec.env && typeof spec.env === 'object' ? spec.env : null
115
+ const envKeys = env ? Object.keys(env) : []
116
+ if (envKeys.length > 0) {
117
+ lines.push('')
118
+ lines.push(`[mcp_servers.${serverId}.env]`)
119
+ for (const key of envKeys) {
120
+ lines.push(`${tomlKey(key)} = ${tomlString(env[key])}`)
121
+ }
122
+ }
123
+
124
+ const block = lines.join('\n')
125
+
126
+ // Preserve existing content; separate the new block with exactly one blank
127
+ // line. An empty existing file yields just the block + trailing newline.
128
+ // Strip the existing file's trailing newline run first so a file that
129
+ // already ends in a blank line (`…\n\n`) does not produce a DOUBLE blank
130
+ // line before the appended block.
131
+ let content
132
+ if (existing.trim().length === 0) {
133
+ content = `${block}\n`
134
+ } else {
135
+ const head = existing.replace(/\n+$/, '')
136
+ content = `${head}\n\n${block}\n`
137
+ }
138
+ return { content, action: 'written' }
139
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * util/events-path.mjs — canonical path computation for the per-day event
3
+ * log at `.aperant/events/{YYYY-MM-DD}.jsonl` (ID-05).
4
+ *
5
+ * Lives in util/ so both the install layer (install/mcp-provision.mjs) and the
6
+ * verify-proof layer (verify-proof/audit.mjs) compute the same path WITHOUT
7
+ * install/ importing from verify-proof/ (the layer-boundary rule). Pure
8
+ * function — no fs, no exec.
9
+ */
10
+
11
+ import { join, resolve } from 'node:path'
12
+
13
+ /**
14
+ * Compute today's events filename relative to projectDir.
15
+ * @param {string} projectDir
16
+ * @param {Date} [now]
17
+ * @returns {string}
18
+ */
19
+ export function dailyEventsPath(projectDir, now = new Date()) {
20
+ const yyyy = now.getUTCFullYear()
21
+ const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
22
+ const dd = String(now.getUTCDate()).padStart(2, '0')
23
+ return join(resolve(projectDir), '.aperant/events', `${yyyy}-${mm}-${dd}.jsonl`)
24
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * util/runtime-capabilities.mjs — pure project-shape capability detection.
3
+ *
4
+ * Extracted from packages/core/src/prompts/prompt-loader.ts:572-595 so the
5
+ * framework CLI is self-contained (no @aperant/core dep). Same heuristics:
6
+ * read package.json's dependencies + devDependencies, return a capability
7
+ * record the resolver can score against.
8
+ *
9
+ * Lives in util/ so BOTH the verify-proof layer (verify-proof/runtime-detect.mjs
10
+ * re-exports it) AND the install layer (install/runtime-detect.mjs re-exports
11
+ * it) can share one implementation without install/ importing from
12
+ * verify-proof/ (the layer-boundary rule).
13
+ *
14
+ * Pattern: pure data in → pure data out. No filesystem walking, no exec.
15
+ * Use loadRuntimeCapabilities(projectDir) for the cwd-rooted variant.
16
+ *
17
+ * v0.8.6 — `loadRuntimeCapabilities` now ALSO walks `apps/*` by default
18
+ * (configurable via the second arg `{monorepo_globs}`) and OR-merges
19
+ * each match's `package.json` capabilities with the root. The default
20
+ * `['apps/*']` matches the canonical Aperant monorepo layout; adopters
21
+ * whose monorepo lives elsewhere (e.g. `packages/apps/*`) pass a
22
+ * custom `monorepo_globs`. Setting `monorepo_globs: []` disables the
23
+ * walk entirely (root-only escape hatch).
24
+ */
25
+
26
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
27
+ import { join, resolve } from 'node:path'
28
+
29
+ /**
30
+ * @typedef {Object} RuntimeCapabilities
31
+ * @property {boolean} is_electron
32
+ * @property {boolean} is_tauri
33
+ * @property {boolean} is_nextjs
34
+ * @property {boolean} is_nuxt
35
+ * @property {boolean} is_web_frontend
36
+ * @property {boolean} is_expo
37
+ * @property {boolean} is_react_native
38
+ * @property {boolean} has_node
39
+ */
40
+
41
+ /** A capability record with every flag defaulted to false. */
42
+ function emptyCapabilities() {
43
+ return {
44
+ is_electron: false,
45
+ is_tauri: false,
46
+ is_nextjs: false,
47
+ is_nuxt: false,
48
+ is_web_frontend: false,
49
+ is_expo: false,
50
+ is_react_native: false,
51
+ has_node: false,
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Detect runtime capabilities from a parsed package.json object.
57
+ *
58
+ * @param {Record<string, unknown> | null} packageJson
59
+ * @returns {RuntimeCapabilities}
60
+ */
61
+ export function detectFromPackageJson(packageJson) {
62
+ const capabilities = emptyCapabilities()
63
+ if (packageJson === null || typeof packageJson !== 'object') return capabilities
64
+
65
+ const allDeps = {
66
+ ...(packageJson.dependencies ?? {}) /** @type {Record<string,string>} */,
67
+ ...(packageJson.devDependencies ?? {}) /** @type {Record<string,string>} */,
68
+ }
69
+ const deps = new Set(Object.keys(allDeps).map((k) => k.toLowerCase()))
70
+
71
+ if (deps.has('electron') || [...deps].some((d) => d.startsWith('@electron'))) {
72
+ capabilities.is_electron = true
73
+ }
74
+ if (deps.has('@tauri-apps/api') || deps.has('tauri')) {
75
+ capabilities.is_tauri = true
76
+ }
77
+ if (deps.has('expo')) capabilities.is_expo = true
78
+ if (deps.has('react-native')) capabilities.is_react_native = true
79
+
80
+ // Web frontends — react/vue/svelte/angular/solid → frontend. BUT: an
81
+ // Electron app commonly bundles React as its renderer; in that case the
82
+ // runtime is "electron", not "web_frontend". Electron + react-native +
83
+ // expo + tauri all win over the web flag.
84
+ const isNativeContainer =
85
+ capabilities.is_electron ||
86
+ capabilities.is_tauri ||
87
+ capabilities.is_react_native ||
88
+ capabilities.is_expo
89
+ const webFrameworks = ['react', 'vue', 'svelte', 'angular', 'solid']
90
+ if (!isNativeContainer && webFrameworks.some((f) => deps.has(f))) {
91
+ capabilities.is_web_frontend = true
92
+ }
93
+
94
+ if (deps.has('next')) {
95
+ capabilities.is_nextjs = true
96
+ capabilities.is_web_frontend = true
97
+ }
98
+ if (deps.has('nuxt')) {
99
+ capabilities.is_nuxt = true
100
+ capabilities.is_web_frontend = true
101
+ }
102
+ if (deps.has('vite') && !isNativeContainer) {
103
+ capabilities.is_web_frontend = true
104
+ }
105
+
106
+ // Node-ish? Anything with a package.json + an entry point counts.
107
+ if (typeof packageJson.main === 'string' || typeof packageJson.module === 'string') {
108
+ capabilities.has_node = true
109
+ }
110
+ if (!capabilities.has_node && packageJson.scripts && typeof packageJson.scripts === 'object') {
111
+ capabilities.has_node = true
112
+ }
113
+
114
+ return capabilities
115
+ }
116
+
117
+ /**
118
+ * Load and detect capabilities from a project directory's package.json.
119
+ * v0.8.6 — also walks `monorepo_globs` (default `['apps/*']`) and
120
+ * OR-merges each match's capabilities with the root.
121
+ *
122
+ * @param {string} projectDir
123
+ * @param {{monorepo_globs?: string[]}} [opts]
124
+ * @returns {RuntimeCapabilities}
125
+ */
126
+ export function loadRuntimeCapabilities(projectDir, opts) {
127
+ const root = resolve(projectDir)
128
+ const rootPkgPath = join(root, 'package.json')
129
+ let rootCaps = emptyCapabilities()
130
+ if (existsSync(rootPkgPath)) {
131
+ try {
132
+ rootCaps = detectFromPackageJson(JSON.parse(readFileSync(rootPkgPath, 'utf-8')))
133
+ } catch {
134
+ /* fall through with empty caps */
135
+ }
136
+ }
137
+
138
+ // Default to the canonical Aperant monorepo layout. Passing `[]`
139
+ // disables the walk entirely (root-only escape hatch per spec ID-05).
140
+ const globs = opts && Array.isArray(opts.monorepo_globs) ? opts.monorepo_globs : ['apps/*']
141
+ if (globs.length === 0) return rootCaps
142
+
143
+ let merged = rootCaps
144
+ for (const glob of globs) {
145
+ const matches = expandSimpleGlob(root, glob)
146
+ for (const dir of matches) {
147
+ const childPkgPath = join(dir, 'package.json')
148
+ if (!existsSync(childPkgPath)) continue
149
+ try {
150
+ const childCaps = detectFromPackageJson(JSON.parse(readFileSync(childPkgPath, 'utf-8')))
151
+ merged = orMergeCapabilities(merged, childCaps)
152
+ } catch {
153
+ /* skip malformed package.json */
154
+ }
155
+ }
156
+ }
157
+ return merged
158
+ }
159
+
160
+ /**
161
+ * Expand a simple `prefix/*` glob against the root. Supports ONLY the
162
+ * `<dir>/*` shape (matches each direct subdirectory of `<dir>`). Other
163
+ * glob syntax is intentionally rejected — v1 keeps the dep surface
164
+ * small. Fails closed on malformed patterns (returns []).
165
+ *
166
+ * @param {string} root
167
+ * @param {string} glob
168
+ * @returns {string[]}
169
+ */
170
+ function expandSimpleGlob(root, glob) {
171
+ if (typeof glob !== 'string' || !glob.endsWith('/*')) return []
172
+ const prefix = glob.slice(0, -2)
173
+ if (prefix.length === 0 || prefix.includes('*')) return []
174
+ const dir = join(root, prefix)
175
+ if (!existsSync(dir)) return []
176
+ let entries
177
+ try {
178
+ entries = readdirSync(dir)
179
+ } catch {
180
+ return []
181
+ }
182
+ const matches = []
183
+ for (const name of entries) {
184
+ const sub = join(dir, name)
185
+ try {
186
+ if (statSync(sub).isDirectory()) matches.push(sub)
187
+ } catch {
188
+ /* skip unreadable entries */
189
+ }
190
+ }
191
+ return matches
192
+ }
193
+
194
+ /**
195
+ * Boolean OR-merge of two capability records. Any flag true in either
196
+ * input is true in the output. The "Electron wins over web" rule from
197
+ * `detectFromPackageJson` is preserved when this is called repeatedly —
198
+ * each child's flags are honest about that child's own shape, and the
199
+ * OR-merge just unions them.
200
+ *
201
+ * @param {RuntimeCapabilities} a
202
+ * @param {RuntimeCapabilities} b
203
+ * @returns {RuntimeCapabilities}
204
+ */
205
+ function orMergeCapabilities(a, b) {
206
+ return {
207
+ is_electron: a.is_electron || b.is_electron,
208
+ is_tauri: a.is_tauri || b.is_tauri,
209
+ is_nextjs: a.is_nextjs || b.is_nextjs,
210
+ is_nuxt: a.is_nuxt || b.is_nuxt,
211
+ is_web_frontend: a.is_web_frontend || b.is_web_frontend,
212
+ is_expo: a.is_expo || b.is_expo,
213
+ is_react_native: a.is_react_native || b.is_react_native,
214
+ has_node: a.has_node || b.has_node,
215
+ }
216
+ }
@@ -12,21 +12,14 @@
12
12
  * lines but no line is ever overwritten.
13
13
  */
14
14
 
15
- import { appendFileSync, mkdirSync } from 'node:fs'
16
- import { dirname, join, resolve } from 'node:path'
15
+ import { appendJsonl } from '../util/atomic-write.mjs'
16
+ import { dailyEventsPath } from '../util/events-path.mjs'
17
17
 
18
- /**
19
- * Compute today's events filename relative to projectDir.
20
- * @param {string} projectDir
21
- * @param {Date} [now]
22
- * @returns {string}
23
- */
24
- export function dailyEventsPath(projectDir, now = new Date()) {
25
- const yyyy = now.getUTCFullYear()
26
- const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
27
- const dd = String(now.getUTCDate()).padStart(2, '0')
28
- return join(resolve(projectDir), '.aperant/events', `${yyyy}-${mm}-${dd}.jsonl`)
29
- }
18
+ // dailyEventsPath now lives in util/events-path.mjs (so install/ can share it
19
+ // without crossing into verify-proof/). Re-exported here byte-for-byte so
20
+ // every existing `import { dailyEventsPath } from '../verify-proof/audit.mjs'`
21
+ // call site keeps working.
22
+ export { dailyEventsPath }
30
23
 
31
24
  /**
32
25
  * Append a driver-invocation audit line.
@@ -45,11 +38,12 @@ export function dailyEventsPath(projectDir, now = new Date()) {
45
38
  */
46
39
  export function appendDriverEvent(projectDir, entry) {
47
40
  const path = dailyEventsPath(projectDir)
48
- mkdirSync(dirname(path), { recursive: true })
49
- const line = JSON.stringify({
41
+ // Single canonical jsonl-append primitive for the event log (appendJsonl
42
+ // creates parent dirs, fsyncs, and locks on Windows). Keeps every writer to
43
+ // .aperant/events/{date}.jsonl on the same atomic path.
44
+ appendJsonl(path, {
50
45
  ts: new Date().toISOString(),
51
46
  kind: 'driver_invocation',
52
47
  ...entry,
53
48
  })
54
- appendFileSync(path, line + '\n', 'utf-8')
55
49
  }
@@ -1,211 +1,18 @@
1
1
  /**
2
- * runtime-detect.mjs — pure project-shape capability detection.
2
+ * verify-proof/runtime-detect.mjs — pure project-shape capability detection.
3
3
  *
4
- * Extracted from packages/core/src/prompts/prompt-loader.ts:572-595 so the
5
- * framework CLI is self-contained (no @aperant/core dep). Same heuristics:
6
- * read package.json's dependencies + devDependencies, return a capability
7
- * record the resolver can score against.
8
- *
9
- * Pattern: pure data in → pure data out. No filesystem walking, no exec.
10
- * Use loadRuntimeCapabilities(projectDir) for the cwd-rooted variant.
11
- *
12
- * v0.8.6 — `loadRuntimeCapabilities` now ALSO walks `apps/*` by default
13
- * (configurable via the second arg `{monorepo_globs}`) and OR-merges
14
- * each match's `package.json` capabilities with the root. The default
15
- * `['apps/*']` matches the canonical Aperant monorepo layout; adopters
16
- * whose monorepo lives elsewhere (e.g. `packages/apps/*`) pass a
17
- * custom `monorepo_globs`. Setting `monorepo_globs: []` disables the
18
- * walk entirely (root-only escape hatch).
4
+ * The implementation now lives in util/runtime-capabilities.mjs so the install
5
+ * layer can share it WITHOUT install/ importing from verify-proof/ (the
6
+ * layer-boundary rule). Re-exported here byte-for-byte so every existing
7
+ * `import { } from '../verify-proof/runtime-detect.mjs'` call site (and the
8
+ * `RuntimeCapabilities` typedef references) keep working unchanged.
19
9
  */
20
10
 
21
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
22
- import { join, resolve } from 'node:path'
11
+ export {
12
+ detectFromPackageJson,
13
+ loadRuntimeCapabilities,
14
+ } from '../util/runtime-capabilities.mjs'
23
15
 
24
16
  /**
25
- * @typedef {Object} RuntimeCapabilities
26
- * @property {boolean} is_electron
27
- * @property {boolean} is_tauri
28
- * @property {boolean} is_nextjs
29
- * @property {boolean} is_nuxt
30
- * @property {boolean} is_web_frontend
31
- * @property {boolean} is_expo
32
- * @property {boolean} is_react_native
33
- * @property {boolean} has_node
34
- */
35
-
36
- /** A capability record with every flag defaulted to false. */
37
- function emptyCapabilities() {
38
- return {
39
- is_electron: false,
40
- is_tauri: false,
41
- is_nextjs: false,
42
- is_nuxt: false,
43
- is_web_frontend: false,
44
- is_expo: false,
45
- is_react_native: false,
46
- has_node: false,
47
- }
48
- }
49
-
50
- /**
51
- * Detect runtime capabilities from a parsed package.json object.
52
- *
53
- * @param {Record<string, unknown> | null} packageJson
54
- * @returns {RuntimeCapabilities}
55
- */
56
- export function detectFromPackageJson(packageJson) {
57
- const capabilities = emptyCapabilities()
58
- if (packageJson === null || typeof packageJson !== 'object') return capabilities
59
-
60
- const allDeps = {
61
- ...(packageJson.dependencies ?? {}) /** @type {Record<string,string>} */,
62
- ...(packageJson.devDependencies ?? {}) /** @type {Record<string,string>} */,
63
- }
64
- const deps = new Set(Object.keys(allDeps).map((k) => k.toLowerCase()))
65
-
66
- if (deps.has('electron') || [...deps].some((d) => d.startsWith('@electron'))) {
67
- capabilities.is_electron = true
68
- }
69
- if (deps.has('@tauri-apps/api') || deps.has('tauri')) {
70
- capabilities.is_tauri = true
71
- }
72
- if (deps.has('expo')) capabilities.is_expo = true
73
- if (deps.has('react-native')) capabilities.is_react_native = true
74
-
75
- // Web frontends — react/vue/svelte/angular/solid → frontend. BUT: an
76
- // Electron app commonly bundles React as its renderer; in that case the
77
- // runtime is "electron", not "web_frontend". Electron + react-native +
78
- // expo + tauri all win over the web flag.
79
- const isNativeContainer =
80
- capabilities.is_electron ||
81
- capabilities.is_tauri ||
82
- capabilities.is_react_native ||
83
- capabilities.is_expo
84
- const webFrameworks = ['react', 'vue', 'svelte', 'angular', 'solid']
85
- if (!isNativeContainer && webFrameworks.some((f) => deps.has(f))) {
86
- capabilities.is_web_frontend = true
87
- }
88
-
89
- if (deps.has('next')) {
90
- capabilities.is_nextjs = true
91
- capabilities.is_web_frontend = true
92
- }
93
- if (deps.has('nuxt')) {
94
- capabilities.is_nuxt = true
95
- capabilities.is_web_frontend = true
96
- }
97
- if (deps.has('vite') && !isNativeContainer) {
98
- capabilities.is_web_frontend = true
99
- }
100
-
101
- // Node-ish? Anything with a package.json + an entry point counts.
102
- if (typeof packageJson.main === 'string' || typeof packageJson.module === 'string') {
103
- capabilities.has_node = true
104
- }
105
- if (!capabilities.has_node && packageJson.scripts && typeof packageJson.scripts === 'object') {
106
- capabilities.has_node = true
107
- }
108
-
109
- return capabilities
110
- }
111
-
112
- /**
113
- * Load and detect capabilities from a project directory's package.json.
114
- * v0.8.6 — also walks `monorepo_globs` (default `['apps/*']`) and
115
- * OR-merges each match's capabilities with the root.
116
- *
117
- * @param {string} projectDir
118
- * @param {{monorepo_globs?: string[]}} [opts]
119
- * @returns {RuntimeCapabilities}
120
- */
121
- export function loadRuntimeCapabilities(projectDir, opts) {
122
- const root = resolve(projectDir)
123
- const rootPkgPath = join(root, 'package.json')
124
- let rootCaps = emptyCapabilities()
125
- if (existsSync(rootPkgPath)) {
126
- try {
127
- rootCaps = detectFromPackageJson(JSON.parse(readFileSync(rootPkgPath, 'utf-8')))
128
- } catch {
129
- /* fall through with empty caps */
130
- }
131
- }
132
-
133
- // Default to the canonical Aperant monorepo layout. Passing `[]`
134
- // disables the walk entirely (root-only escape hatch per spec ID-05).
135
- const globs = opts && Array.isArray(opts.monorepo_globs) ? opts.monorepo_globs : ['apps/*']
136
- if (globs.length === 0) return rootCaps
137
-
138
- let merged = rootCaps
139
- for (const glob of globs) {
140
- const matches = expandSimpleGlob(root, glob)
141
- for (const dir of matches) {
142
- const childPkgPath = join(dir, 'package.json')
143
- if (!existsSync(childPkgPath)) continue
144
- try {
145
- const childCaps = detectFromPackageJson(JSON.parse(readFileSync(childPkgPath, 'utf-8')))
146
- merged = orMergeCapabilities(merged, childCaps)
147
- } catch {
148
- /* skip malformed package.json */
149
- }
150
- }
151
- }
152
- return merged
153
- }
154
-
155
- /**
156
- * Expand a simple `prefix/*` glob against the root. Supports ONLY the
157
- * `<dir>/*` shape (matches each direct subdirectory of `<dir>`). Other
158
- * glob syntax is intentionally rejected — v1 keeps the dep surface
159
- * small. Fails closed on malformed patterns (returns []).
160
- *
161
- * @param {string} root
162
- * @param {string} glob
163
- * @returns {string[]}
164
- */
165
- function expandSimpleGlob(root, glob) {
166
- if (typeof glob !== 'string' || !glob.endsWith('/*')) return []
167
- const prefix = glob.slice(0, -2)
168
- if (prefix.length === 0 || prefix.includes('*')) return []
169
- const dir = join(root, prefix)
170
- if (!existsSync(dir)) return []
171
- let entries
172
- try {
173
- entries = readdirSync(dir)
174
- } catch {
175
- return []
176
- }
177
- const matches = []
178
- for (const name of entries) {
179
- const sub = join(dir, name)
180
- try {
181
- if (statSync(sub).isDirectory()) matches.push(sub)
182
- } catch {
183
- /* skip unreadable entries */
184
- }
185
- }
186
- return matches
187
- }
188
-
189
- /**
190
- * Boolean OR-merge of two capability records. Any flag true in either
191
- * input is true in the output. The "Electron wins over web" rule from
192
- * `detectFromPackageJson` is preserved when this is called repeatedly —
193
- * each child's flags are honest about that child's own shape, and the
194
- * OR-merge just unions them.
195
- *
196
- * @param {RuntimeCapabilities} a
197
- * @param {RuntimeCapabilities} b
198
- * @returns {RuntimeCapabilities}
17
+ * @typedef {import('../util/runtime-capabilities.mjs').RuntimeCapabilities} RuntimeCapabilities
199
18
  */
200
- function orMergeCapabilities(a, b) {
201
- return {
202
- is_electron: a.is_electron || b.is_electron,
203
- is_tauri: a.is_tauri || b.is_tauri,
204
- is_nextjs: a.is_nextjs || b.is_nextjs,
205
- is_nuxt: a.is_nuxt || b.is_nuxt,
206
- is_web_frontend: a.is_web_frontend || b.is_web_frontend,
207
- is_expo: a.is_expo || b.is_expo,
208
- is_react_native: a.is_react_native || b.is_react_native,
209
- has_node: a.has_node || b.has_node,
210
- }
211
- }
@@ -42,7 +42,8 @@
42
42
  "report_formats": ["html", "md"],
43
43
  "evidence_capture": true,
44
44
  "max_iterations": 3,
45
- "strict": false
45
+ "strict": false,
46
+ "provision_mcp": true
46
47
  },
47
48
  "multi_model": {
48
49
  "cross_verification": {
@@ -96,6 +97,17 @@
96
97
  "interval_minutes": 30
97
98
  }
98
99
  },
100
+ "collaboration": {
101
+ "$comment": "Collaboration catch-up surfaces shown on interactive (TTY) apt calls — Welcome-Back Summary (your half-finished work, always-on) + Team-Change Digest (teammates' merged PRs since your last pull, gated to share.visibility team/oss). Tune via /apt:setup. See packages/framework/CONTEXT.md + docs/adr/0001-collaboration-summaries-from-git-not-event-log.md.",
102
+ "welcome_back": {
103
+ "enabled": true,
104
+ "threshold_hours": 16
105
+ },
106
+ "team_digest": {
107
+ "enabled": true,
108
+ "max_entries": 7
109
+ }
110
+ },
99
111
  "orchestration": {
100
112
  "phase_delegation": "auto",
101
113
  "review_mode": "auto",