mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-31 09:51:52 +00:00
Add verified provider update preflight and record first self-host trial
This commit is contained in:
1 parent
2a8ec35dac
commit
f72daadcba
17 files changed
+855
-22
No files matched your search
@@ -69,11 +69,60 @@
|
||||
".torch/backlog",
|
||||
".torch/prompts"
|
||||
],
|
||||
"patched": [],
|
||||
"patched": [
|
||||
{
|
||||
"type": "git-info-exclude-line",
|
||||
"path": "/home/user/Documents/GitHub/TORCH/.git/info/exclude",
|
||||
"line": ".task",
|
||||
"installationId": "73b013e7-41d2-493b-a3a7-022c086f059e"
|
||||
}
|
||||
],
|
||||
"external": [
|
||||
{
|
||||
"type": "local-state",
|
||||
"path": "/home/user/.local/share/torch/projects/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54"
|
||||
},
|
||||
{
|
||||
"type": "worktree",
|
||||
"area": "session-manager",
|
||||
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/session-manager",
|
||||
"branch": "torch/session-manager"
|
||||
},
|
||||
{
|
||||
"type": "worktree",
|
||||
"area": "project-kernel",
|
||||
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/project-kernel",
|
||||
"branch": "torch/project-kernel"
|
||||
},
|
||||
{
|
||||
"type": "worktree",
|
||||
"area": "provider-runtime",
|
||||
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/provider-runtime",
|
||||
"branch": "torch/provider-runtime"
|
||||
},
|
||||
{
|
||||
"type": "worktree",
|
||||
"area": "work-integration",
|
||||
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/work-integration",
|
||||
"branch": "torch/work-integration"
|
||||
},
|
||||
{
|
||||
"type": "worktree",
|
||||
"area": "owner-console",
|
||||
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/owner-console",
|
||||
"branch": "torch/owner-console"
|
||||
},
|
||||
{
|
||||
"type": "worktree",
|
||||
"area": "release-self-host",
|
||||
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/release-self-host",
|
||||
"branch": "torch/release-self-host"
|
||||
},
|
||||
{
|
||||
"type": "worktree",
|
||||
"area": "qa",
|
||||
"path": "/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/qa",
|
||||
"branch": "torch/qa"
|
||||
}
|
||||
]
|
||||
}
|
||||
+8
-7
@@ -32,6 +32,7 @@
|
||||
"runtimes": {
|
||||
"default": "codex",
|
||||
"codex": {
|
||||
"updatePolicy": { "mode": "auto", "max_age_hours": 24 },
|
||||
"model": "gpt-6-luna",
|
||||
"reasoning": "high",
|
||||
"launchPolicy": {
|
||||
@@ -237,8 +238,8 @@
|
||||
],
|
||||
"resources": [],
|
||||
"runtime": "codex",
|
||||
"model": null,
|
||||
"reasoning": null,
|
||||
"model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"branch": null,
|
||||
"worktree_name": null
|
||||
},
|
||||
@@ -279,8 +280,8 @@
|
||||
],
|
||||
"resources": [],
|
||||
"runtime": "codex",
|
||||
"model": null,
|
||||
"reasoning": null,
|
||||
"model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"branch": null,
|
||||
"worktree_name": null
|
||||
},
|
||||
@@ -322,8 +323,8 @@
|
||||
],
|
||||
"resources": [],
|
||||
"runtime": "codex",
|
||||
"model": null,
|
||||
"reasoning": null,
|
||||
"model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"branch": null,
|
||||
"worktree_name": null
|
||||
},
|
||||
@@ -479,7 +480,7 @@
|
||||
"browser"
|
||||
],
|
||||
"runtime": "codex",
|
||||
"model": "gpt-6-luna",
|
||||
"model": "gpt-5.6-terra",
|
||||
"reasoning": "high",
|
||||
"branch": null,
|
||||
"worktree_name": null
|
||||
|
||||
@@ -164,6 +164,37 @@ Requirements:
|
||||
- Node.js 22 or later
|
||||
- Git
|
||||
|
||||
### Provider version preflight
|
||||
|
||||
Opt in during installation with `torch install ... --update-runtimes --yes`,
|
||||
or on an existing project with `torch runtimes update-policy --mode auto --yes`.
|
||||
Approved `torch up --yes` (including resume) refreshes the selected built-in
|
||||
providers against the official npm `latest` release, at most once every 24
|
||||
hours. Change the interval with `--max-age-hours 1` through `168`; disable future
|
||||
downloads with `--mode off`. Dry runs never query the registry or install code.
|
||||
Existing projects stay opt-in; `--yes` alone does not authorize new downloads.
|
||||
|
||||
Preview or force a manual update:
|
||||
|
||||
```bash
|
||||
torch runtimes update --runtime codex,claude,pi --dry-run --json
|
||||
torch runtimes update --runtime codex,claude,pi --force --yes --json
|
||||
```
|
||||
|
||||
Updates install exact resolved versions into XDG-local TORCH-owned prefixes,
|
||||
verify `--version` and `--help`, then atomically select their absolute executable
|
||||
paths. Global installations, login files and provider settings are untouched;
|
||||
old versions remain available to already-running processes. Codex and Claude
|
||||
packages can execute installation scripts; Pi uses its current official
|
||||
`@earendil-works/pi-coding-agent` distribution with scripts disabled. Custom
|
||||
adapters require their own reviewed updater. Concurrent updates stop at a lock;
|
||||
failed downloads or verification retain the prior selection and block automatic
|
||||
startup rather than silently claiming the latest version. Interrupted updates
|
||||
leave the lock for inspection, not automatic deletion. Model/account access and
|
||||
adapter flag compatibility remain separate live qualification gates. Persistent
|
||||
schedule and hierarchy-start updater integration remains pending; use `torch up`
|
||||
for this first trial.
|
||||
|
||||
Run the current verification suite:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1225,6 +1225,187 @@ ownership unless it is represented by an allowed durable operation.
|
||||
|
||||
## 14. Runtime adapter contract
|
||||
|
||||
### 14.0 Provider distribution freshness
|
||||
|
||||
Owners MAY approve automatic built-in provider updates at installation with
|
||||
`--update-runtimes`, or later through `torch runtimes update-policy --mode auto
|
||||
--yes`. Without that policy, installation/startup MUST NOT silently acquire
|
||||
new code. Approved normal `torch up --yes`, including resume, MUST refresh the
|
||||
selected providers against their official npm latest stable releases when the
|
||||
last successful check is older than the configured 1–168-hour window (24 hours
|
||||
by default). Preview MUST be offline. Codex, Claude Code and Pi MUST be covered;
|
||||
custom adapters require a separately reviewed distribution strategy.
|
||||
|
||||
TORCH MUST use versioned machine-local prefixes and exact selected executable
|
||||
paths, not assume the first PATH entry is current. Downloads MUST preserve
|
||||
global installs, login state, running processes and user provider settings.
|
||||
The updater MUST serialize writes, validate registry versions, verify the
|
||||
installed version and parser startup, and atomically publish each provider's
|
||||
activation record only after success. Failed or interrupted updates MUST NOT
|
||||
silently fall back while claiming freshness; prior activations and failed
|
||||
installation evidence remain available. This does not establish model access,
|
||||
live adapter compatibility, update-script safety or off-machine recoverability.
|
||||
Package lifecycle execution MUST be visible in the approval preview. Persistent
|
||||
schedule/hierarchy launch paths and platform-specific installers require their
|
||||
own qualification; initial automatic integration is the normal `up` path.
|
||||
|
||||
### 14.0a Traceable production pipelines (planned)
|
||||
|
||||
TORCH MUST support optional project-specific multi-stage production flows,
|
||||
without hard-coding game-art stages or requiring a pipeline in simple projects.
|
||||
Session Architect and managers SHOULD identify recurring handoff failures,
|
||||
repeated stage sequences, approval waits and integrated deliverables as evidence
|
||||
that a pipeline may help. They MUST propose, not silently activate, a definition.
|
||||
Prefer a simple checklist where no durable multi-stage state is justified.
|
||||
|
||||
A pipeline is a versioned contract for producing a deliverable; it is neither
|
||||
a second backlog nor a mandatory new manager. Each definition MUST describe:
|
||||
|
||||
- a concrete outcome and one accountable flow coordinator;
|
||||
- a bounded stage DAG with explicit parallel stages and join prerequisites;
|
||||
- existing specialist implementation owners for each stage;
|
||||
- required input types, immutable versions/hashes, and output contracts;
|
||||
- project invariants, applicable checks, test conditions and scarce resources;
|
||||
- machine acceptance criteria separately from creative/owner approvals;
|
||||
- retry safety, failure escalation, rework routes and stall thresholds;
|
||||
- permitted model/tool profiles and separate spending/asset-generation approval;
|
||||
- external dependencies and honest reproducibility limitations.
|
||||
|
||||
Project constraints such as zero stock primitives, a locked model sheet or an
|
||||
exact UV template MUST be explicit stage invariants. They MUST NOT become
|
||||
generic defaults or be relaxed because a downstream tool finds them inconvenient.
|
||||
Human taste approval MUST NOT be replaced by an automated geometry gate.
|
||||
|
||||
Each deliverable MUST have one persistent pipeline-instance record (for example
|
||||
one rig, character, voice pack, migration, research packet or release). The record
|
||||
MUST pin the definition revision and identify its current accepted outputs,
|
||||
stage attempts, responsible identities, existing backlog task IDs, dependencies,
|
||||
receipts, approval revisions, provenance and unresolved waits. Stage work MUST
|
||||
use the existing authoritative backlog. A pipeline card is a view of that work
|
||||
and its deliverable lineage, not another independently assignable task system.
|
||||
|
||||
A stage becomes eligible only when all predecessor outputs match their pinned
|
||||
contracts and required checks/approvals are valid. Running, output-produced,
|
||||
machine-verified, awaiting-approval, accepted, failed and invalidated MUST remain
|
||||
distinguishable. Task completion or a file's existence MUST NOT imply acceptance.
|
||||
Advancement MUST use revision checks so duplicate handoffs, parallel attempts
|
||||
and concurrent managers cannot advance the same instance twice.
|
||||
|
||||
Changed upstream content MUST invalidate dependent evidence and approvals, not
|
||||
silently reuse them. A definition edit MUST NOT rewrite a running instance's
|
||||
history. Explicit owner-reviewed migration or a new versioned instance is
|
||||
required. Rework MUST retain the rejected outputs, reasons and attempt lineage;
|
||||
rollback MUST preserve history and respect irreversible external effects.
|
||||
|
||||
Provenance MUST include exact code commits where relevant, artifact content
|
||||
digests, reference/UV-map/model-sheet versions, tool/model versions, non-secret
|
||||
generation parameters and check conditions. Prompts and resource-sensitive
|
||||
metadata MUST remain private with redaction and deliberate export. Seeds alone
|
||||
MUST NOT be represented as guaranteeing reproducibility of hosted image/voice
|
||||
models. Generated outputs are immutable recorded evidence; regeneration is a
|
||||
new attempt, even with identical inputs. No generative provider or chargeable
|
||||
service is invoked merely because a definition names it.
|
||||
|
||||
Debugging MUST answer: what is waiting, why, on whom, since when, which input or
|
||||
gate failed, what changed between attempts, and what exact evidence permitted
|
||||
the last transition. Evidence SHOULD support before/after visual review where
|
||||
useful. Checks MAY be reused through explicit input/output capability contracts;
|
||||
TORCH MUST NOT claim a mesh/UV/transcription checker exists just because it is
|
||||
named in a template. Each project supplies and qualifies domain-specific checks.
|
||||
|
||||
Managers MUST surface stalled instances and handoff waits even when individual
|
||||
tasks have recent commits. Waiting on owner, specialist, check resource, external
|
||||
service and upstream rework MUST be separate reasons, not one generic stale
|
||||
clock. Manager direct-report check-ins SHOULD consult these exception views.
|
||||
They MUST NOT approve their own outputs, create approval pressure loops, or
|
||||
invent tasks to keep a pipeline busy.
|
||||
|
||||
The Console SHOULD show deliverable cards, stage timeline/DAG, accepted versions,
|
||||
current owner, age/reason of waits, failed checks, approval actions and evidence.
|
||||
Owners MUST be able to comment on an exact output version, approve within their
|
||||
authority and request rework. The hierarchy coordinates outcomes; specialists
|
||||
retain ownership and direct communication. Existing Art/Production leads MAY
|
||||
coordinate a flow; a new lead requires the existing evidence/owner approval path.
|
||||
|
||||
Initial implementation order (not implemented by this specification update):
|
||||
|
||||
Console design requirements:
|
||||
|
||||
- Show a Pipelines navigation entry only when approved definitions/instances
|
||||
exist. Otherwise show a contextual proposal entry in setup, not an empty
|
||||
production board or an automatically created generic pipeline.
|
||||
- Group instances by pipeline revision. Each row shows deliverable name,
|
||||
current stage(s), coordinator, stage owner, working/waiting duration, waiting
|
||||
reason/party, latest evidence and attention status. A pipeline-stage strip
|
||||
communicates actual accepted, active, waiting and invalidated stages. Parallel
|
||||
active stages MUST NOT be flattened into a falsely linear progress percentage.
|
||||
- Filter by pipeline, deliverable, coordinator, stage owner, wait reason and
|
||||
attention state. Save views through the existing project-scoped view mechanism.
|
||||
A linked backlog task can open its deliverable view; ordinary tasks remain on
|
||||
the Work board. Show a reciprocal link instead of copying task state.
|
||||
- Opening a deliverable shows its actual DAG/timeline, inputs and output
|
||||
previews, acceptance metrics, approval subject/revision, rejection reason,
|
||||
attempt history and upstream/downstream lineage. A "Why is this waiting?"
|
||||
summary identifies the exact unmet prerequisite and permitted next action.
|
||||
- Owner actions use the existing preview/confirmation and authority boundaries:
|
||||
comment on an exact artifact, review an approval, request rework, pause or
|
||||
resume a permitted instance, or review a proposed definition migration.
|
||||
No drag gesture may bypass a stage gate; raw state editing is not an owner
|
||||
convenience feature. A definition/role proposal is never a creation approval.
|
||||
- Retain the existing dark TORCH visual identity, keyboard accessibility,
|
||||
mobile layout, draft/focus preservation and protected refresh. Text/icons
|
||||
must distinguish stage states without relying on color. On mobile show the
|
||||
current stage and blocker first, then expand the full flow. Failed snapshot
|
||||
loads preserve the last evidence with an explicit stale timestamp.
|
||||
- Aggregate per-stage active versus waiting time, rework count, categorized
|
||||
check/approval failures, measured usage/cost and evidence coverage. Unknown
|
||||
cost remains unknown; wall-clock time is not active processing time. Compare
|
||||
metrics only for compatible definition versions and explain denominators.
|
||||
- After a configurable sample (five completed instances is a starting point,
|
||||
not a universal rule), a manager MAY propose one bottleneck improvement with
|
||||
baseline, expected metric change and review window. Changes require the same
|
||||
definition-review boundary; existing instances stay pinned. Avoid claiming
|
||||
causal improvements from small or incompatible samples.
|
||||
- The shared Console demo MUST use the same renderer with clearly labelled
|
||||
fixture evidence. It MUST NOT call live provider APIs or represent fixture
|
||||
approvals and metrics as real project measurements.
|
||||
|
||||
Example only (project-defined stages, not a built-in rig template):
|
||||
|
||||
```text
|
||||
Pipelines / Rig production
|
||||
Deliverable Stage Waiting on Since Owner
|
||||
Bastion Model sheet review Art approval 2 days Concept Art
|
||||
Wayfarer Texture check UV coverage 18 min Textures
|
||||
|
||||
Bastion / definition v2 / attempt 3
|
||||
Spec -> Concept -> Model sheet -> Mesh -> Texture -> Integration
|
||||
awaiting approval
|
||||
Inputs: approved concept digest / locked proportions digest
|
||||
Output: model-sheet digest + preview
|
||||
Required: proportion check passed; Art Director approval pending
|
||||
[Review evidence] [Comment] [Request rework]
|
||||
History: attempt 2 rejected, silhouette too wide; retained for comparison
|
||||
```
|
||||
|
||||
This view is planned, not a currently available Console feature.
|
||||
|
||||
1. Definition/instance schemas, validation, read-only pipeline assessment and
|
||||
owner-reviewed activation; reject cycles, unknown owners/checks and overlaps.
|
||||
2. Backlog-linked durable transitions, pinned artifacts, checks/approvals and
|
||||
output-specific feedback, with rework and idempotent crash recovery.
|
||||
3. Why-waiting and stall diagnostics plus manager exception reporting.
|
||||
4. Console deliverable cards and attempt/lineage comparison.
|
||||
5. TORCH self-host pilot on a docs/software pipeline; COMBATRIG art integration
|
||||
remains a separate project-authorized adoption.
|
||||
|
||||
Required acceptance scenarios MUST prove that a stale UV/reference input blocks
|
||||
downstream advancement; changed output invalidates approval; duplicate handoffs
|
||||
advance once; a parallel join waits for every accepted branch; rejection preserves
|
||||
history; restart resumes the same stage attempt; resource waits are not mistaken
|
||||
for productive work; and an owner decision cannot be replaced by an AI manager.
|
||||
No pipeline runtime or COMBATRIG mutation is authorized by documenting this plan.
|
||||
|
||||
Each adapter SHOULD implement capabilities equivalent to:
|
||||
|
||||
```text
|
||||
|
||||
@@ -2195,3 +2195,45 @@ test_integrity_note:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
# Provider update preflight (2026-09-30)
|
||||
|
||||
Self-host packaging follow-up: extended the existing SCN-candidate-isolation
|
||||
fixture with a tracked `.torch/torch.yaml`; assert it is absent from the engine
|
||||
candidate while committed engine files and dependency symlinks remain present.
|
||||
This adds a stricter no-project-state-shipping invariant without changing any
|
||||
existing assertion. Update policy is validated under `runtimes.<name>.updatePolicy`,
|
||||
which older readers already treat as extensible runtime data; no incompatible
|
||||
top-level project key or permanent global provider setting is introduced.
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: new_tests
|
||||
scenarios:
|
||||
- id: SCN-provider-updates
|
||||
given: approved selected built-in providers and deterministic registry
|
||||
when: latest versions install and pass parser/version smoke checks
|
||||
then: registry launches exact managed binaries and fresh startup is offline
|
||||
- id: SCN-provider-update-cli
|
||||
given: installed project with owner-enabled update policy
|
||||
when: dry run, actual startup and failed stale refresh execute
|
||||
then: preview is offline, actual launch uses updated binary, failure cannot launch
|
||||
- id: SCN-provider-updates-recovery
|
||||
given: prior verified activation
|
||||
when: download fails, another updater owns lock or executable changes
|
||||
then: prior selection survives failure and unsafe or concurrent use is refused
|
||||
- id: SCN-provider-update-safety
|
||||
given: fixed official package allowlist and owned prefixes
|
||||
when: arbitrary name, injected version, escaped prefix or symlink store appears
|
||||
then: updater refuses without arbitrary install execution
|
||||
observable_outcomes:
|
||||
- persisted selected versions and exact launch commands
|
||||
- refusal codes and absence of downloads during preview or fresh-cache use
|
||||
determinism_controls:
|
||||
- isolated XDG roots, local Git fixtures and boundary installer runner
|
||||
- no provider accounts or live registry in regression tests
|
||||
anti_cheat_rationale:
|
||||
prevents: [unverified activation, global overwrite, silent stale fallback, arbitrary command injection]
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
@@ -1,5 +1,60 @@
|
||||
# TORCH remaining work
|
||||
|
||||
## First real self-host run (2026-09-30)
|
||||
|
||||
### Portable production pipelines (new roadmap element)
|
||||
|
||||
- [ ] Detect justified recurring multi-stage flows from repo/spec and handoff
|
||||
evidence; propose owner-reviewed pipeline definitions without auto-creation.
|
||||
- [ ] Define versioned stage DAGs with one flow coordinator, specialist owners,
|
||||
pinned inputs/outputs, project invariants, checks, resources and approval roles.
|
||||
- [ ] Track deliverable instances through the existing backlog/artifact/check/
|
||||
approval systems; do not create another independently scheduled task queue.
|
||||
- [ ] Implement revision-checked transitions, provenance, attempt lineage,
|
||||
invalidation, parallel joins, rework and idempotent interrupted-stage recovery.
|
||||
- [ ] Add why-waiting/stall diagnostics and manager exception check-ins for
|
||||
handoffs and deliverables, not just stale task commits.
|
||||
- [ ] Add Console deliverable cards, stage timeline, exact-output feedback,
|
||||
approval/rework actions and before/after evidence.
|
||||
- [ ] Add conditional Pipelines navigation, per-definition deliverable rows,
|
||||
reciprocal Work-board links, why-waiting drilldown, parallel stage view,
|
||||
accessible mobile/current-blocker presentation and protected refresh.
|
||||
- [ ] Add evidence-backed stage metrics and configurable sample-based reviews;
|
||||
keep unknown costs explicit and preserve definition-version comparisons.
|
||||
Use the same renderer for private live state and clearly labelled demo fixtures.
|
||||
- [ ] Pilot a TORCH software/docs pipeline, then separately qualify project-
|
||||
supplied mesh, UV-paintback, transcription or other domain checks as needed.
|
||||
Canonical contract/acceptance plan: spec section 14.0a. No runtime implemented yet.
|
||||
|
||||
- [x] Install accepted `0.1.0-alpha.0` into the normal version store, activate
|
||||
stable launcher, install six-domain configuration and create seven worktrees.
|
||||
- [x] Verify ChatGPT sign-in, Luna specialist startup and Sol 6.1 manager startup
|
||||
using the current installed Codex 0.159.2 binary. Durable specialist message,
|
||||
manager acknowledgement and runtime IDs observed. No development dispatch,
|
||||
pushes, deployments or host timers enabled.
|
||||
- [x] Add owner-approved install/startup provider version preflight for Codex,
|
||||
Claude Code and Pi: exact managed binaries, 24-hour freshness, serialized
|
||||
updates, retained previous installs, offline previews and fail-closed errors.
|
||||
Four deterministic scenarios added; live download qualification recorded in
|
||||
the first-run audit, not implied by mocked installer tests.
|
||||
- [ ] Fix `torch brief` invoked from a managed worktree: both live sessions hit
|
||||
`LOCAL_STATE_METADATA_MISMATCH`. Resolve canonical project binding with
|
||||
checked worktree membership, never weaken root/installation identity checks.
|
||||
- [ ] Surface Codex JSON stdout failure events: first Sol launch hid the actual
|
||||
model rejection behind an unrelated inherited Vercel MCP authentication error.
|
||||
- [ ] Add exact executable/version/model/auth preflight to owner Console and
|
||||
setup flow. PATH selected CLI 0.158.0 while the current session used 0.159.2;
|
||||
the older child rejected Sol 6.1 and required manual investigation/retry.
|
||||
- [ ] Make configuration checkpoint/worktree/manifest handoffs explicit or
|
||||
guided: generated tracked files and provisioning manifest updates require
|
||||
deliberate commits; a healthy doctor does not prove startup readiness.
|
||||
- [ ] Isolate irrelevant inherited global MCP servers without removing user
|
||||
settings. Vercel auth noise appeared in both failed and successful Codex runs.
|
||||
- [ ] Integrate approved update policy into hierarchy activation and persistent
|
||||
schedule wakes; qualify interrupted-update recovery and Windows npm execution.
|
||||
- [ ] Qualify current Pi flag/extension/login compatibility after its official
|
||||
package migration; a verified binary `--help` is not a full adapter proof.
|
||||
|
||||
## Review corrections (2026-09-29)
|
||||
|
||||
## Fresh readiness audit (2026-09-30)
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "torch-agent-fleet",
|
||||
"version": "0.1.0-alpha.0",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "torch-agent-fleet",
|
||||
"version": "0.1.0-alpha.0",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"bundleDependencies": [
|
||||
"@modelcontextprotocol/server",
|
||||
"typebox",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "torch-agent-fleet",
|
||||
"version": "0.1.0-alpha.0",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"type": "module",
|
||||
"description": "TORCH — a portable, provider-independent AI development fleet for Git repositories",
|
||||
"main": "src/cli.mjs",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# TORCH first real installation and startup
|
||||
|
||||
## Outcome
|
||||
|
||||
The tool is installed into TORCH itself. The accepted alpha.0 engine and stable
|
||||
launcher were activated, six specialist domains plus Session Manager received
|
||||
isolated worktrees, and ChatGPT-authenticated Codex turns ran for Owner Console
|
||||
(Luna/high) and Session Manager (Sol 6.1/high). The specialist sent a durable
|
||||
message; the manager acknowledged it and sent a reply. Both durable runtime IDs
|
||||
were captured. Empty backlog and idle are preserved; no development dispatched.
|
||||
|
||||
This is an installation/communication smoke test, not full release qualification.
|
||||
Both sessions reported a worktree CLI binding problem rather than bypassing it.
|
||||
No provider API key was read or installed. No pushes, deployment, COMBATRIG
|
||||
changes, persistent host timers or automatic claims were enabled.
|
||||
|
||||
## Configuration
|
||||
|
||||
- Project ID: `3dc11932-6e3c-42aa-9a8a-ca7b8f818b54`.
|
||||
- Launcher: `/home/user/.local/bin/torch`.
|
||||
- Project state: `.torch/` plus XDG-local project history.
|
||||
- Worktrees: `/home/user/TORCHWorktrees/3dc11932-6e3c-42aa-9a8a-ca7b8f818b54/`.
|
||||
- Session Manager: `gpt-6.1-sol`, high.
|
||||
- Kernel, Runtime, Work/Integration and QA: `gpt-5.6-terra`, high, not launched.
|
||||
Interpreted "Tera" as the account's exact Terra model; owner clarification
|
||||
can change those project profiles without code/global-settings changes.
|
||||
- Owner Console and Release/Self-host: `gpt-6-luna`, high.
|
||||
- Proposed domain leads remain inactive; this was not a two-domain-manager trial.
|
||||
- Dashboard: `http://127.0.0.1:4174/console`, localhost only. HTML and actual
|
||||
`/api/snapshot` returned HTTP 200 with installed project identity.
|
||||
|
||||
The protected fixture is intact. A reversible anchored `.git/info/exclude`
|
||||
entry keeps it outside ownership and clean-checkout scheduling. No tracked
|
||||
global ignore or user-file deletion was used. Initial `.torch` checkpoint:
|
||||
`2a8ec35dac1c4b603297176fd1a8dcce36300d2b`.
|
||||
|
||||
## Real friction and its cost
|
||||
|
||||
1. PATH selected Codex 0.158.0 while this parent session used bundled 0.159.2.
|
||||
The older child rejected `gpt-6.1-sol` with an account-support error. One
|
||||
failed launch and a diagnostic replay preceded the correct executable retry.
|
||||
This did not establish a subscription-wide lack of Sol access. Current CLI
|
||||
0.159.2 subsequently ran the requested model successfully.
|
||||
2. Startup errors omit Codex JSON stdout events. The displayed error was a
|
||||
global Vercel MCP authentication failure, hiding the actual model rejection.
|
||||
Manual capture of provider stdout was required. Unrelated MCP noise also
|
||||
appeared in successful startup; it is not TORCH's communication server.
|
||||
3. `torch brief` from both managed worktrees returned
|
||||
`LOCAL_STATE_METADATA_MISMATCH`. Bound MCP queries/identity/ownership/inbox/
|
||||
backlog worked, and the agents correctly reported the failure and stopped.
|
||||
Fix must prove canonical-root/worktree membership rather than relaxing checks.
|
||||
4. Generated `.torch` state requires a local commit before worktree creation;
|
||||
provisioning then changes the tracked installation manifest. This requires
|
||||
another checkpoint. Current docs don't guide that complete handoff clearly.
|
||||
5. `doctor` is healthy despite an unavailable requested model or worktree-brief
|
||||
failure. Installation integrity is not live startup/model qualification.
|
||||
6. Manager should not receive specialist-only next-task instructions. In this
|
||||
smoke policy the manager query was correctly refused; separate role-specific
|
||||
startup steps would avoid an unnecessary error.
|
||||
|
||||
The plumbing works, but setup still requires engineering knowledge. It is not
|
||||
yet an effortless end-user onboarding experience. The above incidents are in
|
||||
`docs/TODO.md`, with distinct evidence and no claims of fixes not yet delivered.
|
||||
|
||||
## Provider-update improvement implemented during the first run
|
||||
|
||||
Owner explicitly requested current provider harnesses at install/startup.
|
||||
New update preflight stages exact versions into TORCH-owned prefixes, verifies
|
||||
version and parser startup, serializes updates, retains prior installs, and
|
||||
selects absolute executable paths. Login/global settings are unchanged.
|
||||
Automatic policy is owner-approved and per configured runtime, 24-hour freshness;
|
||||
offline previews and failed refreshes do not launch stale providers silently.
|
||||
|
||||
Actual official-registry install/parser receipts:
|
||||
|
||||
- Codex `0.159.2`.
|
||||
- Claude Code `2.1.285`.
|
||||
- Pi `0.99.1` via current `@earendil-works/pi-coding-agent` package.
|
||||
|
||||
Receipt: `/tmp/torch-first-run-provider-update-result.json`. All three verified
|
||||
`--version` and `--help`. Only Codex has live session qualification in this run.
|
||||
Current Pi extension/flags and Claude lifecycle remain separately unqualified;
|
||||
a newer binary doesn't establish adapter compatibility or model access.
|
||||
|
||||
Four new deterministic scenarios exercise official allowlist, exact executable,
|
||||
fresh-cache/no-network behavior, fail-closed refresh, locking, tamper detection,
|
||||
path safety and real CLI policy/startup boundaries. Existing self-host packaging
|
||||
scenario now also excludes tracked project `.torch` state from engine candidates.
|
||||
All 39 source acceptance groups PASS before checkpoint; installed candidate
|
||||
qualification is recorded separately after build/activation.
|
||||
|
||||
## Pipeline addition: planned, not implemented
|
||||
|
||||
Spec section 14.0a now defines versioned optional project production flows,
|
||||
deliverable instances linked to the existing backlog, pinned artifact/check/
|
||||
approval lineage, safe rework, explicit waits, and a conditional Console view.
|
||||
Frontend-design guidance shaped the row-to-detail progression, mobile blocker-
|
||||
first presentation and evidence hierarchy without changing existing Console CSS.
|
||||
There is no live pipeline runtime/view yet and no COMBATRIG pipeline alteration.
|
||||
|
||||
## Evidence receipts
|
||||
|
||||
- `/tmp/torch-real-candidate-build-20260930.json`
|
||||
- `/tmp/torch-real-first-run-install.json`
|
||||
- `/tmp/torch-first-run-model-list.json`
|
||||
- `/tmp/torch-first-run-worktrees.json`
|
||||
- `/tmp/torch-first-run-manager-start.json` (old CLI failure)
|
||||
- `/tmp/torch-first-run-manager-diagnostic.json` (actual model error)
|
||||
- `/tmp/torch-first-run-manager-current-cli-start.json` (Sol success)
|
||||
- `/tmp/torch-first-run-luna-start.json` (Luna success)
|
||||
- `/tmp/torch-first-run-alpha1-final-acceptance.json`
|
||||
|
||||
No cache savings, token cost or active-time metric is claimed: no attributable
|
||||
runtime usage samples have been submitted. Pipeline metrics remain a design.
|
||||
@@ -4,13 +4,14 @@ import { createCodexAdapter } from './codex.mjs';
|
||||
import { createPiAdapter } from './pi.mjs';
|
||||
import { validateRuntimeAdapter } from './runtime.mjs';
|
||||
import { inspectRuntimePluginTrust, loadTrustedRuntimeAdapters } from './plugins.mjs';
|
||||
import { managedProvider } from './updates.mjs';
|
||||
|
||||
export class RuntimeAdapterRegistry {
|
||||
constructor({ env = process.env, adapters = [], loadPlugins = false, pluginNames } = {}) {
|
||||
this.adapters = new Map([
|
||||
['claude', createClaudeAdapter({ env })],
|
||||
['codex', createCodexAdapter({ env })],
|
||||
['pi', createPiAdapter({ env })],
|
||||
['claude', createClaudeAdapter({ env, executable: managedProvider('claude', { env })?.executable ?? 'claude' })],
|
||||
['codex', createCodexAdapter({ env, executable: managedProvider('codex', { env })?.executable ?? 'codex' })],
|
||||
['pi', createPiAdapter({ env, executable: managedProvider('pi', { env })?.executable ?? 'pi' })],
|
||||
]);
|
||||
this.pluginErrors = [];
|
||||
this.trustedPlugins = [];
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { accessSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, relative, resolve, sep } from 'node:path';
|
||||
import { torchDataHome } from '../kernel/paths.mjs';
|
||||
import { TorchError } from '../kernel/errors.mjs';
|
||||
|
||||
// Fixed official distribution names. Project input cannot supply npm packages,
|
||||
// registries, scripts, prefixes or alternate update commands.
|
||||
export const PROVIDER_PACKAGES = Object.freeze({
|
||||
codex: '@openai/codex', claude: '@anthropic-ai/claude-code',
|
||||
pi: '@earendil-works/pi-coding-agent',
|
||||
});
|
||||
const REGISTRY = 'https://registry.npmjs.org';
|
||||
const VERSION = /^\d+\.\d+\.\d+$/;
|
||||
|
||||
function fail(message, details) {
|
||||
return new TorchError(message, { code: 'RUNTIME_UPDATE_FAILED', details });
|
||||
}
|
||||
|
||||
function safePath(path) {
|
||||
let current = resolve(path);
|
||||
for (;;) {
|
||||
if (existsSync(current) && lstatSync(current).isSymbolicLink()) {
|
||||
throw fail('Provider store must not traverse symlink directories', { path: current });
|
||||
}
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export function providerStore(env = process.env) {
|
||||
return join(torchDataHome(env), 'provider-runtimes');
|
||||
}
|
||||
|
||||
function pointer(root, name) { return join(root, `${name}.json`); }
|
||||
function hash(path) { return createHash('sha256').update(readFileSync(path)).digest('hex'); }
|
||||
|
||||
export function managedProvider(name, { env = process.env } = {}) {
|
||||
if (!Object.hasOwn(PROVIDER_PACKAGES, name)) return null;
|
||||
const root = providerStore(env);
|
||||
safePath(root);
|
||||
const path = pointer(root, name);
|
||||
if (!existsSync(path)) return null;
|
||||
safePath(path);
|
||||
let record;
|
||||
try { record = JSON.parse(readFileSync(path, 'utf8')); } catch {
|
||||
throw fail('Provider activation record is unreadable', { name });
|
||||
}
|
||||
const prefix = resolve(record.prefix ?? '');
|
||||
if (record.name !== name || record.package !== PROVIDER_PACKAGES[name]
|
||||
|| !VERSION.test(record.version ?? '') || dirname(prefix) !== resolve(root)
|
||||
|| !prefix.startsWith(join(root, `${name}-${record.version}-`))) {
|
||||
throw fail('Provider activation record is invalid', { name });
|
||||
}
|
||||
safePath(prefix);
|
||||
const executable = join(prefix, 'node_modules', '.bin', name);
|
||||
try {
|
||||
const target = realpathSync(executable);
|
||||
if (relative(prefix, target).startsWith(`..${sep}`) || !target.startsWith(`${prefix}${sep}`)) throw Error('outside prefix');
|
||||
accessSync(executable, constants.X_OK);
|
||||
if (hash(target) !== record.sha256) throw Error('changed executable');
|
||||
} catch {
|
||||
throw fail('Managed provider executable is missing or changed', { name, executable });
|
||||
}
|
||||
return { ...record, executable };
|
||||
}
|
||||
|
||||
export function planProviderUpdates(names, { env = process.env } = {}) {
|
||||
return {
|
||||
action: 'provider-update', scope: 'torch-managed-only', registry: REGISTRY,
|
||||
providers: [...new Set(names)].map((name) => {
|
||||
if (!Object.hasOwn(PROVIDER_PACKAGES, name)) {
|
||||
throw fail('Automatic updates support only built-in providers', { name });
|
||||
}
|
||||
return { name, package: PROVIDER_PACKAGES[name], active: managedProvider(name, { env }) };
|
||||
}),
|
||||
root: providerStore(env), networkRequired: true, executesPackageInstallScripts: true,
|
||||
modifiesGlobalInstallations: false, mutationPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateProviders(names, { env = process.env, runner = spawnSync,
|
||||
authorized = false, force = false, maxAgeHours = 24, now = () => new Date() } = {}) {
|
||||
if (!authorized) throw new TorchError('Provider downloads require owner approval', { code: 'APPROVAL_REQUIRED' });
|
||||
if (!Number.isInteger(maxAgeHours) || maxAgeHours < 1 || maxAgeHours > 168) {
|
||||
throw fail('Provider freshness must be 1–168 hours');
|
||||
}
|
||||
const plan = planProviderUpdates(names, { env });
|
||||
safePath(plan.root);
|
||||
mkdirSync(plan.root, { recursive: true, mode: 0o700 });
|
||||
const lock = join(plan.root, 'update.lock');
|
||||
try { mkdirSync(lock); } catch {
|
||||
throw new TorchError('Provider update already running; inspect update.lock before recovery', { code: 'RUNTIME_UPDATE_BUSY' });
|
||||
}
|
||||
const run = (command, args, cwd, timeout) => {
|
||||
const result = runner(command, args, { cwd, env, encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'], timeout, maxBuffer: 8 * 1024 * 1024 });
|
||||
if (result.status !== 0 || result.error || result.signal) {
|
||||
throw fail('Provider update command failed; previous activation retained', {
|
||||
command, args, status: result.status, signal: result.signal,
|
||||
message: result.error?.message, stderr: String(result.stderr ?? '').slice(-2000),
|
||||
});
|
||||
}
|
||||
return String(result.stdout ?? '').trim();
|
||||
};
|
||||
const results = [];
|
||||
try {
|
||||
for (const provider of plan.providers) {
|
||||
const { name, package: pkg, active } = provider;
|
||||
const age = active ? now().getTime() - Date.parse(active.checkedAt) : NaN;
|
||||
if (!force && Number.isFinite(age) && age >= 0 && age < maxAgeHours * 3600000) {
|
||||
results.push({ ...active, outcome: 'fresh' }); continue;
|
||||
}
|
||||
const version = JSON.parse(run('npm', ['view', `${pkg}@latest`, 'version', '--json', '--registry', REGISTRY], plan.root, 30000));
|
||||
if (typeof version !== 'string' || !VERSION.test(version)) throw fail('Registry returned an invalid stable version', { name });
|
||||
let record = active;
|
||||
if (!active || active.version !== version) {
|
||||
const prefix = mkdtempSync(join(plan.root, `${name}-${version}-`));
|
||||
const args = ['install', '--prefix', prefix, '--no-audit', '--no-fund', '--save-exact', '--registry', REGISTRY];
|
||||
if (name === 'pi') args.push('--ignore-scripts');
|
||||
args.push(`${pkg}@${version}`);
|
||||
run('npm', args, prefix, 180000);
|
||||
const executable = join(prefix, 'node_modules', '.bin', name);
|
||||
const target = realpathSync(executable);
|
||||
if (!target.startsWith(`${prefix}${sep}`)) throw fail('Provider binary escaped its owned prefix', { name });
|
||||
const actual = run(executable, ['--version'], prefix, 20000);
|
||||
if (!actual.match(/\b\d+\.\d+\.\d+\b/) || actual.match(/\b\d+\.\d+\.\d+\b/)[0] !== version) {
|
||||
throw fail('Installed provider version does not match the resolved release', { name, expected: version, actual });
|
||||
}
|
||||
// Smoke-check parser startup before activation. Real model/account and
|
||||
// adapter-specific compatibility still require their own live gates.
|
||||
run(executable, ['--help'], prefix, 20000);
|
||||
record = { name, package: pkg, version, prefix, sha256: hash(target), previous: active?.prefix ?? null };
|
||||
}
|
||||
record = { ...record, checkedAt: now().toISOString() };
|
||||
const temporary = join(plan.root, `${name}-${randomUUID()}.tmp`);
|
||||
writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
||||
renameSync(temporary, pointer(plan.root, name));
|
||||
results.push({ ...managedProvider(name, { env }), outcome: active?.version === version ? 'current' : 'updated' });
|
||||
}
|
||||
return { ...plan, results, mutationPerformed: true };
|
||||
} finally {
|
||||
rmdirSync(lock);
|
||||
}
|
||||
}
|
||||
+67
-5
@@ -3,6 +3,7 @@ import { BacklogService } from './backlog/service.mjs';
|
||||
import { ArtifactService } from './artifacts/service.mjs';
|
||||
import { createClaudeAdapter } from './adapters/claude.mjs';
|
||||
import { createRuntimeAdapterRegistry } from './adapters/registry.mjs';
|
||||
import { planProviderUpdates, updateProviders } from './adapters/updates.mjs';
|
||||
import { profileCapabilityIssues, resolveLaunchPolicy } from './adapters/runtime.mjs';
|
||||
import {
|
||||
inspectRuntimePluginTrust, planRuntimePluginRevoke, planRuntimePluginTrust,
|
||||
@@ -69,7 +70,7 @@ Usage:
|
||||
torch architect validate --brief <path> --response <path> [--json]
|
||||
torch architect run --brief <path> --provider <claude|codex> --output <path> [--model <model>] [--max-budget-usd <amount>] --yes [--json]
|
||||
torch domains [--repo <path>] [--spec <path>] [--output <path>] [--json]
|
||||
torch install --proposal <path> [--runtime <adapter,adapter>] [--default-runtime <adapter>] [--dry-run] [--yes] [--json]
|
||||
torch install --proposal <path> [--runtime <adapter,adapter>] [--default-runtime <adapter>] [--update-runtimes] [--dry-run] [--yes] [--json]
|
||||
torch worktrees [--parent <path>] [--dry-run] [--yes] [--json]
|
||||
torch up [--fresh] [--only <id,id>] [--dry-run] [--yes] [--json]
|
||||
torch down [--dry-run] [--yes] [--json]
|
||||
@@ -91,6 +92,8 @@ Usage:
|
||||
torch profile set --area <id> [--runtime <adapter>] [--model <model>|--inherit-model] [--reasoning <level|none>] [--launch-policy <field=value> ...] [--reset|--reset-launch-policy] [--dry-run|--yes] [--json]
|
||||
torch profile defaults --runtime <adapter> [--model <model>] [--reasoning <level|none>] [--launch-policy <field=value> ...] [--reset|--reset-launch-policy] --yes [--json]
|
||||
torch runtimes list [--json]
|
||||
torch runtimes update --runtime <codex,claude,pi> [--force] [--dry-run|--yes] [--json]
|
||||
torch runtimes update-policy --mode <auto|off> [--max-age-hours <1-168>] --yes [--json]
|
||||
torch runtimes trust --name <id> --module </absolute/path/adapter.cjs> [--yes --sha256 <reviewed-hash>] [--json]
|
||||
torch runtimes revoke --name <id> [--yes --sha256 <reviewed-hash>] [--json]
|
||||
torch complete --area <id> --summary <text> [--task <id>] [--evidence <text>] [--commit <sha>] [--json]
|
||||
@@ -415,6 +418,7 @@ export const CANDIDATE_ACCEPTANCE_EVIDENCE = Object.freeze({
|
||||
'branches-worktrees': ['SCN-worktree-bootstrap'],
|
||||
'runtime-identities': ['SCN-mixed-runtime'],
|
||||
'runtime-profiles': ['SCN-runtime-profiles', 'SCN-runtime-cost-ceiling', 'SCN-runtime-profile-cli'],
|
||||
'provider-updates': ['SCN-provider-updates', 'SCN-provider-updates-recovery', 'SCN-provider-update-safety', 'SCN-provider-update-cli'],
|
||||
'console-runtime-profile': ['SCN-console-runtime-profile'],
|
||||
'console-schedule-launcher': ['SCN-console-schedule-launcher'],
|
||||
'runtime-adapters': [
|
||||
@@ -552,6 +556,38 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
}
|
||||
if (command === 'runtimes') {
|
||||
const operation = argv[1] ?? 'list';
|
||||
if (operation === 'update') {
|
||||
const names = commaList(optionValue(argv, '--runtime'));
|
||||
if (!names.length) throw new TorchError('Select --runtime codex,claude,pi', { code: 'RUNTIME_UPDATE_INVALID' });
|
||||
const plan = planProviderUpdates(names, { env });
|
||||
if (argv.includes('--dry-run') || !argv.includes('--yes')) {
|
||||
print({ ...plan, requiresApproval: true }, { json }); return 0;
|
||||
}
|
||||
print(updateProviders(names, { env, runner: spawn, authorized: true, force: argv.includes('--force') }), { json });
|
||||
return 0;
|
||||
}
|
||||
if (operation === 'update-policy') {
|
||||
const repository = inspectRepository(optionValue(argv, '--repo') ?? cwd);
|
||||
const config = loadProjectConfig(repository.root);
|
||||
const mode = optionValue(argv, '--mode');
|
||||
const maxAge = Number(optionValue(argv, '--max-age-hours') ?? 24);
|
||||
const policy = { mode, max_age_hours: maxAge };
|
||||
const names = optionValue(argv, '--runtime') === undefined
|
||||
? Object.keys(config.runtimes).filter(name => name !== 'default')
|
||||
: commaList(optionValue(argv, '--runtime'));
|
||||
planProviderUpdates(names, { env });
|
||||
for (const name of names) {
|
||||
if (!Object.hasOwn(config.runtimes, name)) throw new TorchError('Runtime is not configured', { code: 'RUNTIME_UPDATE_INVALID', details: { name } });
|
||||
config.runtimes[name].updatePolicy = policy;
|
||||
}
|
||||
validateProjectConfig(config);
|
||||
if (argv.includes('--dry-run') || !argv.includes('--yes')) {
|
||||
print({ policy, runtimes: names, requiresApproval: true, mutationPerformed: false }, { json }); return 0;
|
||||
}
|
||||
print({ path: persistProfileConfig(repository.root, config), policy, runtimes: names,
|
||||
sessionsStarted: false, mutationPerformed: true }, { json });
|
||||
return 0;
|
||||
}
|
||||
if (operation === 'list') {
|
||||
const builtIns = createRuntimeAdapterRegistry({ env, loadPlugins: false }).names();
|
||||
let trust;
|
||||
@@ -967,12 +1003,15 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
const runtimes = runtimeOption === undefined ? undefined : commaList(runtimeOption);
|
||||
if (argv.includes('--dry-run')) {
|
||||
const proposal = loadProposal(cwd, proposalPath);
|
||||
print({ ...planInstall({
|
||||
const plan = planInstall({
|
||||
repository, proposal, env, runtimes, defaultRuntime,
|
||||
runtimeRegistry: createRuntimeAdapterRegistry({
|
||||
env, loadPlugins: true, pluginNames: [...(runtimes ?? proposalRuntimeNames(proposal)), ...[defaultRuntime].filter(Boolean)],
|
||||
}),
|
||||
}), proposal: resolve(cwd, proposalPath) }, { json });
|
||||
});
|
||||
print({ ...plan, proposal: resolve(cwd, proposalPath),
|
||||
...(argv.includes('--update-runtimes') ? { providerUpdates: planProviderUpdates(plan.runtimes, { env }) } : {}),
|
||||
}, { json });
|
||||
return 0;
|
||||
}
|
||||
if (!argv.includes('--yes')) {
|
||||
@@ -981,12 +1020,25 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
});
|
||||
}
|
||||
const proposal = loadProposal(cwd, proposalPath);
|
||||
print(installProject({
|
||||
if (argv.includes('--update-runtimes')) {
|
||||
const installPlan = planInstall({ repository, proposal, env, runtimes, defaultRuntime });
|
||||
if (!installPlan.canProceed) throw new TorchError('Install plan has blockers', { code: 'INSTALL_BLOCKED', details: installPlan.blockers });
|
||||
updateProviders(installPlan.runtimes, { env, runner: spawn, authorized: true });
|
||||
}
|
||||
const installed = installProject({
|
||||
repository, proposal, env, runtimes, defaultRuntime,
|
||||
runtimeRegistry: createRuntimeAdapterRegistry({
|
||||
env, loadPlugins: true, pluginNames: [...(runtimes ?? proposalRuntimeNames(proposal)), ...[defaultRuntime].filter(Boolean)],
|
||||
}),
|
||||
}), { json });
|
||||
});
|
||||
if (argv.includes('--update-runtimes')) {
|
||||
const config = loadProjectConfig(repository.root);
|
||||
for (const name of Object.keys(config.runtimes).filter(name => name !== 'default')) {
|
||||
config.runtimes[name].updatePolicy = { mode: 'auto', max_age_hours: 24 };
|
||||
}
|
||||
persistProfileConfig(repository.root, config);
|
||||
}
|
||||
print(installed, { json });
|
||||
return 0;
|
||||
}
|
||||
if (command === 'doctor') {
|
||||
@@ -1010,6 +1062,16 @@ export async function runCli(argv = process.argv.slice(2), {
|
||||
return 0;
|
||||
}
|
||||
if (command === 'up') {
|
||||
const updateConfig = loadProjectConfig(repository.root);
|
||||
if (argv.includes('--yes') && !argv.includes('--dry-run')) {
|
||||
const selected = optionValue(argv, '--only') === undefined ? undefined : commaList(optionValue(argv, '--only'));
|
||||
for (const name of configuredRuntimeNames(repository.root, selected)) {
|
||||
const policy = updateConfig.runtimes[name]?.updatePolicy;
|
||||
if (policy?.mode === 'auto') updateProviders([name], {
|
||||
env, runner: spawn, authorized: true, maxAgeHours: policy.max_age_hours,
|
||||
});
|
||||
}
|
||||
}
|
||||
const control = openControlPlane({ repositoryRoot: repository.root, env });
|
||||
try {
|
||||
const only = optionValue(argv, '--only') === undefined ? undefined : commaList(optionValue(argv, '--only'));
|
||||
|
||||
@@ -320,6 +320,13 @@ export const projectConfigSchema = z.object({
|
||||
}
|
||||
}
|
||||
for (const [runtime, profile] of Object.entries(config.runtimes)) {
|
||||
if (runtime !== 'default' && profile?.updatePolicy !== undefined) {
|
||||
const policy = z.object({ mode: z.enum(['auto', 'off']),
|
||||
max_age_hours: z.number().int().min(1).max(168) }).strict().safeParse(profile.updatePolicy);
|
||||
if (!policy.success) for (const issue of policy.error.issues) {
|
||||
context.addIssue({ ...issue, path: ['runtimes', runtime, 'updatePolicy', ...issue.path] });
|
||||
}
|
||||
}
|
||||
if (runtime === 'default' || !profile || typeof profile !== 'object' || Array.isArray(profile)) continue;
|
||||
if (profile.launchPolicy !== undefined) {
|
||||
const result = launchPolicy.safeParse(profile.launchPolicy);
|
||||
|
||||
@@ -23,6 +23,7 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([
|
||||
'branches-worktrees',
|
||||
'runtime-identities',
|
||||
'runtime-profiles',
|
||||
'provider-updates',
|
||||
'console-runtime-profile',
|
||||
'console-schedule-launcher',
|
||||
'runtime-adapters',
|
||||
@@ -150,10 +151,10 @@ function gitSourceDescriptor(root) {
|
||||
const excludedUntracked = execFileSync('git', ['-C', root, 'ls-files', '--others', '--exclude-standard', '-z'], {
|
||||
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}).split('\0').filter(Boolean).sort();
|
||||
const include = (name, entry) => name === 'node_modules'
|
||||
const include = (name, entry) => name !== '.torch' && !name.startsWith('.torch/') && (name === 'node_modules'
|
||||
|| name.startsWith('node_modules/')
|
||||
|| tracked.has(name)
|
||||
|| (entry.isDirectory() && [...tracked].some((path) => path.startsWith(`${name}/`)));
|
||||
|| (entry.isDirectory() && [...tracked].some((path) => path.startsWith(`${name}/`))));
|
||||
return { mode: 'git-commit', commit, include, excludedUntracked };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { managedProvider, planProviderUpdates, providerStore, updateProviders } from '../../src/adapters/updates.mjs';
|
||||
import { createRuntimeAdapterRegistry } from '../../src/adapters/registry.mjs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { inspectRepository } from '../../src/kernel/git.mjs';
|
||||
import { analyzeRepository } from '../../src/kernel/analyze.mjs';
|
||||
import { proposeDomains } from '../../src/kernel/domains.mjs';
|
||||
import { installProject } from '../../src/kernel/install.mjs';
|
||||
import { createWorktrees } from '../../src/kernel/worktrees.mjs';
|
||||
import { runCli } from '../../src/cli.mjs';
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'torch-provider-update-'));
|
||||
const env = { ...process.env, XDG_DATA_HOME: root };
|
||||
const calls = [];
|
||||
let version = '1.2.3';
|
||||
let failInstall = false;
|
||||
const runner = (command, args, options) => {
|
||||
calls.push({ command, args, options });
|
||||
if (args[0] === 'view') return { status: 0, stdout: JSON.stringify(version) };
|
||||
if (args[0] === 'install') {
|
||||
if (failInstall) return { status: 1, stderr: 'offline' };
|
||||
const pkg = args.at(-1);
|
||||
const name = pkg.startsWith('@openai/') ? 'codex' : pkg.startsWith('@anthropic-ai/') ? 'claude' : 'pi';
|
||||
const prefix = args[args.indexOf('--prefix') + 1];
|
||||
mkdirSync(join(prefix, 'node_modules', '.bin'), { recursive: true });
|
||||
const path = join(prefix, 'node_modules', '.bin', name);
|
||||
writeFileSync(path, `#!/bin/sh\n# ${version}\n`);
|
||||
chmodSync(path, 0o755);
|
||||
return { status: 0, stdout: 'installed' };
|
||||
}
|
||||
return { status: 0, stdout: args[0] === '--version' ? `provider ${version}`
|
||||
: args.includes('exec') ? '{"type":"thread.started","thread_id":"provider-update-fixture-session"}\n' : 'help' };
|
||||
};
|
||||
return { env, calls, runner, next: v => { version = v; }, breakInstall: () => { failInstall = true; } };
|
||||
}
|
||||
|
||||
test('SCN-provider-updates: all built-ins resolve official latest, verify and launch the managed binary', () => {
|
||||
const f = fixture();
|
||||
const plan = planProviderUpdates(['codex', 'claude', 'pi'], f);
|
||||
assert.equal(f.calls.length, 0);
|
||||
assert.equal(plan.modifiesGlobalInstallations, false);
|
||||
assert.throws(() => updateProviders(['codex'], f), { code: 'APPROVAL_REQUIRED' });
|
||||
assert.equal(existsSync(providerStore(f.env)), false);
|
||||
const result = updateProviders(['codex', 'claude', 'pi'], { ...f, authorized: true });
|
||||
assert.deepEqual(result.results.map(r => r.outcome), ['updated', 'updated', 'updated']);
|
||||
const registry = createRuntimeAdapterRegistry({ env: f.env });
|
||||
for (const name of ['codex', 'claude', 'pi']) {
|
||||
const selected = managedProvider(name, f);
|
||||
assert.equal(registry.get(name).executable, selected.executable);
|
||||
assert.equal(selected.version, '1.2.3');
|
||||
assert.equal(f.calls.some(c => c.command === selected.executable && c.args[0] === '--version'), true);
|
||||
assert.equal(f.calls.some(c => c.command === selected.executable && c.args[0] === '--help'), true);
|
||||
}
|
||||
for (const call of f.calls.filter(c => c.command === 'npm')) {
|
||||
assert.equal(call.args.includes('-g'), false);
|
||||
assert.equal(call.args.includes('--registry'), true);
|
||||
assert.equal(call.options.timeout > 0, true);
|
||||
}
|
||||
assert.equal(f.calls.find(c => c.args.at(-1) === '@earendil-works/[email protected]').args.includes('--ignore-scripts'), true);
|
||||
const count = f.calls.length;
|
||||
assert.equal(updateProviders(['codex'], { ...f, authorized: true }).results[0].outcome, 'fresh');
|
||||
assert.equal(f.calls.length, count, 'fresh startup performs no network/install calls');
|
||||
});
|
||||
|
||||
test('SCN-provider-update-cli: opted-in startup updates selected provider, dry runs stay offline, failed refresh cannot launch', async () => {
|
||||
const f = fixture();
|
||||
const root = join(f.env.XDG_DATA_HOME, 'repo');
|
||||
mkdirSync(root);
|
||||
const git = args => execFileSync('git', ['-C', root, ...args], { stdio: 'pipe' });
|
||||
git(['init', '-b', 'main']); git(['config', 'user.email', '[email protected]']); git(['config', 'user.name', 'Fixture']);
|
||||
writeFileSync(join(root, 'README.md'), '# Test project\n');
|
||||
git(['add', '.']); git(['commit', '-m', 'fixture']);
|
||||
const repository = inspectRepository(root);
|
||||
const proposal = proposeDomains({ repository, analysis: analyzeRepository(repository) });
|
||||
proposal.review = { status: 'approved', reviewedAt: '2026-09-30T00:00:00Z', reviewedBy: 'fixture-owner', notes: [] };
|
||||
installProject({ repository, proposal, env: f.env, runtimes: ['codex'] });
|
||||
const stdout = process.stdout.write;
|
||||
const outputs = [];
|
||||
process.stdout.write = chunk => { outputs.push(String(chunk)); return true; };
|
||||
try {
|
||||
const run = args => runCli(args, { cwd: root, env: f.env, spawn: f.runner });
|
||||
assert.equal(await run(['runtimes', 'update-policy', '--mode', 'auto', '--yes', '--json']), 0);
|
||||
const config = JSON.parse(readFileSync(join(root, '.torch', 'torch.yaml')));
|
||||
assert.deepEqual(config.runtimes.codex.updatePolicy, { mode: 'auto', max_age_hours: 24 });
|
||||
git(['add', '.torch']); git(['commit', '-m', 'config']);
|
||||
createWorktrees({ repository: inspectRepository(root), parentOverride: join(f.env.XDG_DATA_HOME, 'worktrees') });
|
||||
assert.equal(await run(['up', '--fresh', '--only', 'session-manager', '--dry-run', '--json']), 0);
|
||||
assert.equal(f.calls.length, 0, 'dry-run never downloads or launches');
|
||||
assert.equal(await run(['up', '--fresh', '--only', 'session-manager', '--yes', '--json']), 0);
|
||||
const selected = managedProvider('codex', f);
|
||||
const launches = f.calls.filter(c => c.args.includes('exec'));
|
||||
assert.equal(launches.length, 1);
|
||||
assert.equal(launches[0].command, selected.executable);
|
||||
assert.equal(f.calls.some(c => c.args.at(-1)?.startsWith('@anthropic-ai/')), false);
|
||||
const pointer = join(providerStore(f.env), 'codex.json');
|
||||
const stale = JSON.parse(readFileSync(pointer)); stale.checkedAt = '2000-01-01T00:00:00Z';
|
||||
writeFileSync(pointer, JSON.stringify(stale)); f.next('1.3.0'); f.breakInstall();
|
||||
assert.equal(await run(['up', '--only', 'session-manager', '--yes', '--json']), 2);
|
||||
assert.equal(f.calls.filter(c => c.args.includes('exec')).length, 1, 'failed refresh must not launch the old provider silently');
|
||||
assert.equal(outputs.some(s => s.includes('RUNTIME_UPDATE_FAILED')), true);
|
||||
} finally { process.stdout.write = stdout; }
|
||||
});
|
||||
|
||||
test('SCN-provider-updates-recovery: failed updates retain last activation; tampering and concurrent updates stop', () => {
|
||||
const f = fixture();
|
||||
updateProviders(['codex'], { ...f, authorized: true });
|
||||
const active = managedProvider('codex', f);
|
||||
f.next('1.3.0'); f.breakInstall();
|
||||
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true, force: true }), { code: 'RUNTIME_UPDATE_FAILED' });
|
||||
assert.equal(managedProvider('codex', f).executable, active.executable);
|
||||
assert.equal(managedProvider('codex', f).version, '1.2.3');
|
||||
mkdirSync(join(providerStore(f.env), 'update.lock'));
|
||||
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true }), { code: 'RUNTIME_UPDATE_BUSY' });
|
||||
writeFileSync(active.executable, 'tampered');
|
||||
assert.throws(() => managedProvider('codex', f), { code: 'RUNTIME_UPDATE_FAILED' });
|
||||
});
|
||||
|
||||
test('SCN-provider-update-safety: arbitrary packages, unsafe registry versions, path escape and symlink stores fail closed', () => {
|
||||
const f = fixture();
|
||||
assert.throws(() => planProviderUpdates(['untrusted-command'], f), { code: 'RUNTIME_UPDATE_FAILED' });
|
||||
f.next('1.2.3;touch /tmp/evil');
|
||||
assert.throws(() => updateProviders(['codex'], { ...f, authorized: true }), { code: 'RUNTIME_UPDATE_FAILED' });
|
||||
assert.equal(f.calls.some(c => c.args[0] === 'install'), false);
|
||||
f.next('1.2.3'); updateProviders(['codex'], { ...f, authorized: true });
|
||||
const path = join(providerStore(f.env), 'codex.json');
|
||||
const record = JSON.parse(readFileSync(path));
|
||||
record.prefix = tmpdir(); writeFileSync(path, JSON.stringify(record));
|
||||
assert.throws(() => managedProvider('codex', f), { code: 'RUNTIME_UPDATE_FAILED' });
|
||||
const other = fixture();
|
||||
mkdirSync(join(other.env.XDG_DATA_HOME, 'torch'));
|
||||
symlinkSync(tmpdir(), providerStore(other.env));
|
||||
assert.throws(() => planProviderUpdates(['pi'], other), { code: 'RUNTIME_UPDATE_FAILED' });
|
||||
});
|
||||
@@ -159,6 +159,8 @@ test('SCN-candidate-isolation: staging requires complete acceptance and never ch
|
||||
writeFileSync(join(checkout, 'node_modules', 'fixture-dependency', 'index.js'), 'export const dependency = true;\n');
|
||||
symlinkSync('../fixture-dependency/index.js', join(checkout, 'node_modules', '.bin', 'fixture-dependency'));
|
||||
writeFileSync(join(checkout, '.gitignore'), 'node_modules/\nlocal-secret.env\n');
|
||||
mkdirSync(join(checkout, '.torch'));
|
||||
writeFileSync(join(checkout, '.torch', 'torch.yaml'), '{"machineSpecificProject":true}\n');
|
||||
execFileSync('git', ['init', '-b', 'main', checkout]);
|
||||
execFileSync('git', ['-C', checkout, 'config', 'user.email', '[email protected]']);
|
||||
execFileSync('git', ['-C', checkout, 'config', 'user.name', 'TORCH Test']);
|
||||
@@ -173,6 +175,8 @@ test('SCN-candidate-isolation: staging requires complete acceptance and never ch
|
||||
assert.deepEqual(boundedPlan.excludedUntracked, ['new-runtime-adapter.mjs'],
|
||||
'the reviewed candidate plan names untracked files that will not ship');
|
||||
const bounded = service.installCandidate({ source: checkout, validate: accepted });
|
||||
assert.equal(existsSync(join(bounded.destination, '.torch')), false,
|
||||
'tracked self-host project state must not ship inside the installed engine');
|
||||
assert.equal(existsSync(join(bounded.destination, 'local-secret.env')), false);
|
||||
assert.equal(existsSync(join(bounded.destination, 'new-runtime-adapter.mjs')), false);
|
||||
assert.equal(existsSync(join(bounded.destination, 'node_modules', '.bin', 'fixture-dependency')), true);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": "torch.dev/release/v1alpha1",
|
||||
"version": "0.1.0-alpha.0",
|
||||
"version": "0.1.0-alpha.1",
|
||||
"state": {
|
||||
"reads": ["torch.dev/state/v1alpha1"],
|
||||
"writes": "torch.dev/state/v1alpha1",
|
||||
|
||||
Reference in new issue
Block a user